diff options
Diffstat (limited to 'java/src/com')
286 files changed, 23138 insertions, 29071 deletions
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityLongPressTimer.java b/java/src/com/android/inputmethod/accessibility/AccessibilityLongPressTimer.java new file mode 100644 index 000000000..37d910edb --- /dev/null +++ b/java/src/com/android/inputmethod/accessibility/AccessibilityLongPressTimer.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.accessibility; + +import android.content.Context; +import android.os.Handler; +import android.os.Message; + +import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.latin.R; + +// Handling long press timer to show a more keys keyboard. +final class AccessibilityLongPressTimer extends Handler { + public interface LongPressTimerCallback { + public void performLongClickOn(Key key); + } + + private static final int MSG_LONG_PRESS = 1; + + private final LongPressTimerCallback mCallback; + private final long mConfigAccessibilityLongPressTimeout; + + public AccessibilityLongPressTimer(final LongPressTimerCallback callback, + final Context context) { + super(); + mCallback = callback; + mConfigAccessibilityLongPressTimeout = context.getResources().getInteger( + R.integer.config_accessibility_long_press_key_timeout); + } + + @Override + public void handleMessage(final Message msg) { + switch (msg.what) { + case MSG_LONG_PRESS: + cancelLongPress(); + mCallback.performLongClickOn((Key)msg.obj); + return; + default: + super.handleMessage(msg); + return; + } + } + + public void startLongPress(final Key key) { + cancelLongPress(); + final Message longPressMessage = obtainMessage(MSG_LONG_PRESS, key); + sendMessageDelayed(longPressMessage, mConfigAccessibilityLongPressTimeout); + } + + public void cancelLongPress() { + removeMessages(MSG_LONG_PRESS); + } +} diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java index 10fb9fef4..2762a9f25 100644 --- a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java +++ b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java @@ -17,7 +17,6 @@ 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; @@ -63,13 +62,11 @@ public final class AccessibilityUtils { */ private static final boolean ENABLE_ACCESSIBILITY = true; - public static void init(final InputMethodService inputMethod) { + public static void init(final Context context) { if (!ENABLE_ACCESSIBILITY) return; // These only need to be initialized if the kill switch is off. - sInstance.initInternal(inputMethod); - KeyCodeDescriptionMapper.init(); - AccessibleKeyboardViewProxy.init(inputMethod); + sInstance.initInternal(context); } public static AccessibilityUtils getInstance() { @@ -116,7 +113,7 @@ public final class AccessibilityUtils { * @param event The event to check. * @return {@true} is the event is a touch exploration event */ - public boolean isTouchExplorationEvent(final MotionEvent event) { + public static boolean isTouchExplorationEvent(final MotionEvent event) { final int action = event.getAction(); return action == MotionEvent.ACTION_HOVER_ENTER || action == MotionEvent.ACTION_HOVER_EXIT @@ -158,7 +155,7 @@ public final class AccessibilityUtils { * @param typedWord the currently typed word */ public void setAutoCorrection(final SuggestedWords suggestedWords, final String typedWord) { - if (suggestedWords != null && suggestedWords.mWillAutoCorrect) { + if (suggestedWords.mWillAutoCorrect) { mAutoCorrectionWord = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION); mTypedWord = typedWord; } else { diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java deleted file mode 100644 index 73896dfd3..000000000 --- a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java +++ /dev/null @@ -1,411 +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.accessibility; - -import android.content.Context; -import android.inputmethodservice.InputMethodService; -import android.support.v4.view.AccessibilityDelegateCompat; -import android.support.v4.view.ViewCompat; -import android.support.v4.view.accessibility.AccessibilityEventCompat; -import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; -import android.util.SparseIntArray; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewParent; -import android.view.accessibility.AccessibilityEvent; - -import com.android.inputmethod.keyboard.Key; -import com.android.inputmethod.keyboard.Keyboard; -import com.android.inputmethod.keyboard.KeyboardId; -import com.android.inputmethod.keyboard.MainKeyboardView; -import com.android.inputmethod.keyboard.PointerTracker; -import com.android.inputmethod.latin.R; - -public final class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat { - private static final AccessibleKeyboardViewProxy sInstance = new AccessibleKeyboardViewProxy(); - - /** Map of keyboard modes to resource IDs. */ - private static final SparseIntArray KEYBOARD_MODE_RES_IDS = new SparseIntArray(); - - static { - KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_DATE, R.string.keyboard_mode_date); - KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_DATETIME, R.string.keyboard_mode_date_time); - KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_EMAIL, R.string.keyboard_mode_email); - KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_IM, R.string.keyboard_mode_im); - KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_NUMBER, R.string.keyboard_mode_number); - KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_PHONE, R.string.keyboard_mode_phone); - KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_TEXT, R.string.keyboard_mode_text); - KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_TIME, R.string.keyboard_mode_time); - KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_URL, R.string.keyboard_mode_url); - } - - private InputMethodService mInputMethod; - 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. - */ - private int mEdgeSlop; - - /** The most recently set keyboard mode. */ - private int mLastKeyboardMode; - - public static void init(final InputMethodService inputMethod) { - sInstance.initInternal(inputMethod); - } - - public static AccessibleKeyboardViewProxy getInstance() { - return sInstance; - } - - private AccessibleKeyboardViewProxy() { - // Not publicly instantiable. - } - - private void initInternal(final InputMethodService inputMethod) { - mInputMethod = inputMethod; - mEdgeSlop = inputMethod.getResources().getDimensionPixelSize( - R.dimen.accessibility_edge_slop); - } - - /** - * Sets the view wrapped by this proxy. - * - * @param view The view to wrap. - */ - public void setView(final MainKeyboardView view) { - if (view == null) { - // Ignore null views. - return; - } - mView = view; - - // Ensure that the view has an accessibility delegate. - ViewCompat.setAccessibilityDelegate(view, this); - - if (mAccessibilityNodeProvider == null) { - return; - } - mAccessibilityNodeProvider.setView(view); - } - - /** - * Called when the keyboard layout changes. - * <p> - * <b>Note:</b> This method will be called even if accessibility is not - * enabled. - */ - public void setKeyboard() { - if (mView == null) { - return; - } - if (mAccessibilityNodeProvider != null) { - mAccessibilityNodeProvider.setKeyboard(); - } - final int keyboardMode = mView.getKeyboard().mId.mMode; - - // Since this method is called even when accessibility is off, make sure - // to check the state before announcing anything. Also, don't announce - // changes within the same mode. - if (AccessibilityUtils.getInstance().isAccessibilityEnabled() - && (mLastKeyboardMode != keyboardMode)) { - announceKeyboardMode(keyboardMode); - } - mLastKeyboardMode = keyboardMode; - } - - /** - * Called when the keyboard is hidden and accessibility is enabled. - */ - public void onHideWindow() { - if (mView == null) { - return; - } - announceKeyboardHidden(); - mLastKeyboardMode = -1; - } - - /** - * Announces which type of keyboard is being displayed. If the keyboard type - * is unknown, no announcement is made. - * - * @param mode The new keyboard mode. - */ - private void announceKeyboardMode(int mode) { - final int resId = KEYBOARD_MODE_RES_IDS.get(mode); - if (resId == 0) { - return; - } - final Context context = mView.getContext(); - final String keyboardMode = context.getString(resId); - final String text = context.getString(R.string.announce_keyboard_mode, keyboardMode); - sendWindowStateChanged(text); - } - - /** - * Announces that the keyboard has been hidden. - */ - private void announceKeyboardHidden() { - final Context context = mView.getContext(); - final String text = context.getString(R.string.announce_keyboard_hidden); - - sendWindowStateChanged(text); - } - - /** - * Sends a window state change event with the specified text. - * - * @param text The text to send with the event. - */ - private void sendWindowStateChanged(final String text) { - final AccessibilityEvent stateChange = AccessibilityEvent.obtain( - AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); - mView.onInitializeAccessibilityEvent(stateChange); - stateChange.getText().add(text); - stateChange.setContentDescription(null); - - final ViewParent parent = mView.getParent(); - if (parent != null) { - parent.requestSendAccessibilityEvent(mView, stateChange); - } - } - - /** - * Proxy method for View.getAccessibilityNodeProvider(). This method is called in SDK - * version 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) and higher to obtain the virtual - * node hierarchy provider. - * - * @param host The host view for the provider. - * @return The accessibility node provider for the current keyboard. - */ - @Override - public AccessibilityEntityProvider getAccessibilityNodeProvider(final View host) { - if (mView == null) { - return null; - } - return getAccessibilityNodeProvider(); - } - - /** - * Intercepts touch events before dispatch when touch exploration is turned on in ICS and - * higher. - * - * @param event The motion event being dispatched. - * @return {@code true} if the event is handled - */ - public boolean dispatchTouchEvent(final MotionEvent event) { - // To avoid accidental key presses during touch exploration, always drop - // touch events generated by the user. - return false; - } - - /** - * Receives hover events when touch exploration is turned on in SDK versions ICS and higher. - * - * @param event The hover event. - * @return {@code true} if the event is handled - */ - public boolean dispatchHoverEvent(final MotionEvent event, final PointerTracker tracker) { - if (mView == null) { - return false; - } - - final int x = (int) event.getX(); - final int y = (int) event.getY(); - final Key previousKey = mLastHoverKey; - final Key key; - - if (pointInView(x, y)) { - key = tracker.getKeyOn(x, y); - } else { - key = null; - } - mLastHoverKey = key; - - switch (event.getAction()) { - 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 (key != null) { - getAccessibilityNodeProvider().simulateKeyPress(key); - } - //$FALL-THROUGH$ - case MotionEvent.ACTION_HOVER_ENTER: - return onHoverKey(key, event); - case MotionEvent.ACTION_HOVER_MOVE: - if (key != previousKey) { - return onTransitionKey(key, previousKey, event); - } - return onHoverKey(key, event); - } - return false; - } - - /** - * @return A lazily-instantiated node provider for this view proxy. - */ - private AccessibilityEntityProvider getAccessibilityNodeProvider() { - // Instantiate the provide only when requested. Since the system - // will call this method multiple times it is a good practice to - // cache the provider instance. - if (mAccessibilityNodeProvider == null) { - mAccessibilityNodeProvider = new AccessibilityEntityProvider(mView, mInputMethod); - } - return mAccessibilityNodeProvider; - } - - /** - * Utility method to determine whether the given point, in local coordinates, is inside the - * view, where the area of the view is contracted by the edge slop factor. - * - * @param localX The local x-coordinate. - * @param localY The local y-coordinate. - */ - private boolean pointInView(final int localX, final int localY) { - return (localX >= mEdgeSlop) && (localY >= mEdgeSlop) - && (localX < (mView.getWidth() - mEdgeSlop)) - && (localY < (mView.getHeight() - mEdgeSlop)); - } - - /** - * Simulates a transition between two {@link Key}s by sending a HOVER_EXIT on the previous key, - * a HOVER_ENTER on the current key, and a HOVER_MOVE on the current key. - * - * @param currentKey The currently hovered key. - * @param previousKey The previously hovered key. - * @param event The event that triggered the transition. - * @return {@code true} if the event was handled. - */ - private boolean onTransitionKey(final Key currentKey, final Key previousKey, - final MotionEvent event) { - final int savedAction = event.getAction(); - event.setAction(MotionEvent.ACTION_HOVER_EXIT); - onHoverKey(previousKey, event); - event.setAction(MotionEvent.ACTION_HOVER_ENTER); - onHoverKey(currentKey, event); - event.setAction(MotionEvent.ACTION_HOVER_MOVE); - final boolean handled = onHoverKey(currentKey, event); - event.setAction(savedAction); - return handled; - } - - /** - * Handles a hover event on a key. If {@link Key} extended View, this would be analogous to - * calling View.onHoverEvent(MotionEvent). - * - * @param key The currently hovered key. - * @param event The hover event. - * @return {@code true} if the event was handled. - */ - private boolean onHoverKey(final Key key, final MotionEvent event) { - // Null keys can't receive events. - if (key == null) { - return false; - } - final AccessibilityEntityProvider provider = getAccessibilityNodeProvider(); - - switch (event.getAction()) { - case MotionEvent.ACTION_HOVER_ENTER: - provider.sendAccessibilityEventForKey( - key, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER); - provider.performActionForKey( - key, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null); - break; - case MotionEvent.ACTION_HOVER_EXIT: - provider.sendAccessibilityEventForKey( - key, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT); - break; - } - return true; - } - - /** - * Notifies the user of changes in the keyboard shift state. - */ - public void notifyShiftState() { - if (mView == null) { - return; - } - - final Keyboard keyboard = mView.getKeyboard(); - final KeyboardId keyboardId = keyboard.mId; - final int elementId = keyboardId.mElementId; - final Context context = mView.getContext(); - final CharSequence text; - - switch (elementId) { - case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: - case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED: - text = context.getText(R.string.spoken_description_shiftmode_locked); - break; - case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: - case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: - case KeyboardId.ELEMENT_SYMBOLS_SHIFTED: - text = context.getText(R.string.spoken_description_shiftmode_on); - break; - default: - text = context.getText(R.string.spoken_description_shiftmode_off); - } - AccessibilityUtils.getInstance().announceForAccessibility(mView, text); - } - - /** - * Notifies the user of changes in the keyboard symbols state. - */ - public void notifySymbolsState() { - if (mView == null) { - return; - } - - final Keyboard keyboard = mView.getKeyboard(); - final Context context = mView.getContext(); - final KeyboardId keyboardId = keyboard.mId; - final int elementId = keyboardId.mElementId; - final int resId; - - switch (elementId) { - case KeyboardId.ELEMENT_ALPHABET: - case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: - case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: - case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: - case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED: - resId = R.string.spoken_description_mode_alpha; - break; - case KeyboardId.ELEMENT_SYMBOLS: - case KeyboardId.ELEMENT_SYMBOLS_SHIFTED: - resId = R.string.spoken_description_mode_symbol; - break; - case KeyboardId.ELEMENT_PHONE: - resId = R.string.spoken_description_mode_phone; - break; - case KeyboardId.ELEMENT_PHONE_SYMBOLS: - resId = R.string.spoken_description_mode_phone_shift; - break; - default: - resId = -1; - } - - if (resId < 0) { - return; - } - final String text = context.getString(resId); - 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 58624a2e6..7a3510ee1 100644 --- a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java +++ b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java @@ -17,6 +17,7 @@ package com.android.inputmethod.accessibility; import android.content.Context; +import android.content.res.Resources; import android.text.TextUtils; import android.util.Log; import android.util.SparseIntArray; @@ -27,40 +28,29 @@ import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardId; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.latin.utils.StringUtils; -import java.util.HashMap; +import java.util.Locale; -public final class KeyCodeDescriptionMapper { +final class KeyCodeDescriptionMapper { private static final String TAG = KeyCodeDescriptionMapper.class.getSimpleName(); + private static final String SPOKEN_LETTER_RESOURCE_NAME_FORMAT = "spoken_accented_letter_%04X"; + private static final String SPOKEN_SYMBOL_RESOURCE_NAME_FORMAT = "spoken_symbol_%04X"; + private static final String SPOKEN_EMOJI_RESOURCE_NAME_FORMAT = "spoken_emoji_%04X"; // The resource ID of the string spoken for obscured keys private static final int OBSCURED_KEY_RES_ID = R.string.spoken_description_dot; - private static KeyCodeDescriptionMapper sInstance = new KeyCodeDescriptionMapper(); - - // Map of key labels to spoken description resource IDs - private final HashMap<CharSequence, Integer> mKeyLabelMap = CollectionUtils.newHashMap(); - - // Sparse array of spoken description resource IDs indexed by key codes - private final SparseIntArray mKeyCodeMap; - - public static void init() { - sInstance.initInternal(); - } + private static final KeyCodeDescriptionMapper sInstance = new KeyCodeDescriptionMapper(); public static KeyCodeDescriptionMapper getInstance() { return sInstance; } - private KeyCodeDescriptionMapper() { - mKeyCodeMap = new SparseIntArray(); - } - - private void initInternal() { - // Manual label substitutions for key labels with no string resource - mKeyLabelMap.put(":-)", R.string.spoken_description_smiley); + // Sparse array of spoken description resource IDs indexed by key codes + private final SparseIntArray mKeyCodeMap = new SparseIntArray(); + private KeyCodeDescriptionMapper() { // Special non-character codes defined in Keyboard mKeyCodeMap.put(Constants.CODE_SPACE, R.string.spoken_description_space); mKeyCodeMap.put(Constants.CODE_DELETE, R.string.spoken_description_delete); @@ -75,19 +65,21 @@ public final class KeyCodeDescriptionMapper { mKeyCodeMap.put(Constants.CODE_ACTION_NEXT, R.string.spoken_description_action_next); mKeyCodeMap.put(Constants.CODE_ACTION_PREVIOUS, R.string.spoken_description_action_previous); + mKeyCodeMap.put(Constants.CODE_EMOJI, R.string.spoken_description_emoji); + // Because the upper-case and lower-case mappings of the following letters is depending on + // the locale, the upper case descriptions should be defined here. The lower case + // descriptions are handled in {@link #getSpokenLetterDescriptionId(Context,int)}. + // U+0049: "I" LATIN CAPITAL LETTER I + // U+0069: "i" LATIN SMALL LETTER I + // U+0130: "İ" LATIN CAPITAL LETTER I WITH DOT ABOVE + // U+0131: "ı" LATIN SMALL LETTER DOTLESS I + mKeyCodeMap.put(0x0049, R.string.spoken_letter_0049); + mKeyCodeMap.put(0x0130, R.string.spoken_letter_0130); } /** * Returns the localized description of the action performed by a specified * key based on the current keyboard state. - * <p> - * The order of precedence for key descriptions is: - * <ol> - * <li>Manually-defined based on the key label</li> - * <li>Automatic or manually-defined based on the key code</li> - * <li>Automatically based on the key label</li> - * <li>{code null} for keys with no label or key code defined</li> - * </p> * * @param context The package's context. * @param keyboard The keyboard on which the key resides. @@ -116,18 +108,26 @@ public final class KeyCodeDescriptionMapper { return getDescriptionForActionKey(context, keyboard, key); } - if (!TextUtils.isEmpty(key.getLabel())) { - final String label = key.getLabel().trim(); - - // First, attempt to map the label to a pre-defined description. - if (mKeyLabelMap.containsKey(label)) { - return context.getString(mKeyLabelMap.get(label)); - } + if (code == Constants.CODE_OUTPUT_TEXT) { + return key.getOutputText(); } // Just attempt to speak the description. - if (key.getCode() != Constants.CODE_UNSPECIFIED) { - return getDescriptionForKeyCode(context, keyboard, key, shouldObscure); + if (code != Constants.CODE_UNSPECIFIED) { + // If the key description should be obscured, now is the time to do it. + final boolean isDefinedNonCtrl = Character.isDefined(code) + && !Character.isISOControl(code); + if (shouldObscure && isDefinedNonCtrl) { + return context.getString(OBSCURED_KEY_RES_ID); + } + final String description = getDescriptionForCodePoint(context, code); + if (description != null) { + return description; + } + if (!TextUtils.isEmpty(key.getLabel())) { + return key.getLabel(); + } + return context.getString(R.string.spoken_description_unknown); } return null; } @@ -141,7 +141,7 @@ public final class KeyCodeDescriptionMapper { * @param keyboard The keyboard on which the key resides. * @return a character sequence describing the action performed by pressing the key */ - private String getDescriptionForSwitchAlphaSymbol(final Context context, + private static String getDescriptionForSwitchAlphaSymbol(final Context context, final Keyboard keyboard) { final KeyboardId keyboardId = keyboard.mId; final int elementId = keyboardId.mElementId; @@ -179,7 +179,8 @@ public final class KeyCodeDescriptionMapper { * @param keyboard The keyboard on which the key resides. * @return A context-sensitive description of the "Shift" key. */ - private String getDescriptionForShiftKey(final Context context, final Keyboard keyboard) { + private static String getDescriptionForShiftKey(final Context context, + final Keyboard keyboard) { final KeyboardId keyboardId = keyboard.mId; final int elementId = keyboardId.mElementId; final int resId; @@ -191,9 +192,14 @@ public final class KeyCodeDescriptionMapper { break; case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: - case KeyboardId.ELEMENT_SYMBOLS_SHIFTED: resId = R.string.spoken_description_shift_shifted; break; + case KeyboardId.ELEMENT_SYMBOLS: + resId = R.string.spoken_description_symbols_shift; + break; + case KeyboardId.ELEMENT_SYMBOLS_SHIFTED: + resId = R.string.spoken_description_symbols_shift_shifted; + break; default: resId = R.string.spoken_description_shift; } @@ -208,7 +214,7 @@ public final class KeyCodeDescriptionMapper { * @param key The key to describe. * @return Returns a context-sensitive description of the "Enter" action key. */ - private String getDescriptionForActionKey(final Context context, final Keyboard keyboard, + private static String getDescriptionForActionKey(final Context context, final Keyboard keyboard, final Key key) { final KeyboardId keyboardId = keyboard.mId; final int actionId = keyboardId.imeAction(); @@ -247,42 +253,91 @@ public final class KeyCodeDescriptionMapper { /** * Returns a localized character sequence describing what will happen when - * the specified key is pressed based on its key code. - * <p> - * The order of precedence for key code descriptions is: - * <ol> - * <li>Manually-defined shift-locked description</li> - * <li>Manually-defined shifted description</li> - * <li>Manually-defined normal description</li> - * <li>Automatic based on the character represented by the key code</li> - * <li>Fall-back for undefined or control characters</li> - * </ol> - * </p> + * the specified key is pressed based on its key code point. * * @param context The package's context. - * @param keyboard The keyboard on which the key resides. - * @param key The key from which to obtain a description. - * @param shouldObscure {@true} if text (e.g. non-control) characters should be obscured. - * @return a character sequence describing the action performed by pressing the key + * @param codePoint The code point from which to obtain a description. + * @return a character sequence describing the code point. */ - private String getDescriptionForKeyCode(final Context context, final Keyboard keyboard, - final Key key, final boolean shouldObscure) { - final int code = key.getCode(); - + public String getDescriptionForCodePoint(final Context context, final int codePoint) { // If the key description should be obscured, now is the time to do it. - final boolean isDefinedNonCtrl = Character.isDefined(code) && !Character.isISOControl(code); - if (shouldObscure && isDefinedNonCtrl) { - return context.getString(OBSCURED_KEY_RES_ID); + final int index = mKeyCodeMap.indexOfKey(codePoint); + if (index >= 0) { + return context.getString(mKeyCodeMap.valueAt(index)); } - if (mKeyCodeMap.indexOfKey(code) >= 0) { - return context.getString(mKeyCodeMap.get(code)); + final String accentedLetter = getSpokenAccentedLetterDescription(context, codePoint); + if (accentedLetter != null) { + return accentedLetter; } - if (isDefinedNonCtrl) { - return Character.toString((char) code); + // Here, <code>code</code> may be a base (non-accented) letter. + final String unsupportedSymbol = getSpokenSymbolDescription(context, codePoint); + if (unsupportedSymbol != null) { + return unsupportedSymbol; } - if (!TextUtils.isEmpty(key.getLabel())) { - return key.getLabel(); + final String emojiDescription = getSpokenEmojiDescription(context, codePoint); + if (emojiDescription != null) { + return emojiDescription; + } + if (Character.isDefined(codePoint) && !Character.isISOControl(codePoint)) { + return StringUtils.newSingleCodePointString(codePoint); + } + return null; + } + + // TODO: Remove this method once TTS supports those accented letters' verbalization. + private String getSpokenAccentedLetterDescription(final Context context, final int code) { + final boolean isUpperCase = Character.isUpperCase(code); + final int baseCode = isUpperCase ? Character.toLowerCase(code) : code; + final int baseIndex = mKeyCodeMap.indexOfKey(baseCode); + final int resId = (baseIndex >= 0) ? mKeyCodeMap.valueAt(baseIndex) + : getSpokenDescriptionId(context, baseCode, SPOKEN_LETTER_RESOURCE_NAME_FORMAT); + if (resId == 0) { + return null; + } + final String spokenText = context.getString(resId); + return isUpperCase ? context.getString(R.string.spoken_description_upper_case, spokenText) + : spokenText; + } + + // TODO: Remove this method once TTS supports those symbols' verbalization. + private String getSpokenSymbolDescription(final Context context, final int code) { + final int resId = getSpokenDescriptionId(context, code, SPOKEN_SYMBOL_RESOURCE_NAME_FORMAT); + if (resId == 0) { + return null; + } + final String spokenText = context.getString(resId); + if (!TextUtils.isEmpty(spokenText)) { + return spokenText; + } + // If a translated description is empty, fall back to unknown symbol description. + return context.getString(R.string.spoken_symbol_unknown); + } + + // TODO: Remove this method once TTS supports emoji verbalization. + private String getSpokenEmojiDescription(final Context context, final int code) { + final int resId = getSpokenDescriptionId(context, code, SPOKEN_EMOJI_RESOURCE_NAME_FORMAT); + if (resId == 0) { + return null; + } + final String spokenText = context.getString(resId); + if (!TextUtils.isEmpty(spokenText)) { + return spokenText; + } + // If a translated description is empty, fall back to unknown emoji description. + return context.getString(R.string.spoken_emoji_unknown); + } + + private int getSpokenDescriptionId(final Context context, final int code, + final String resourceNameFormat) { + final String resourceName = String.format(Locale.ROOT, resourceNameFormat, code); + final Resources resources = context.getResources(); + // Note that the resource package name may differ from the context package name. + final String resourcePackageName = resources.getResourcePackageName( + R.string.spoken_description_unknown); + final int resId = resources.getIdentifier(resourceName, "string", resourcePackageName); + if (resId != 0) { + mKeyCodeMap.append(code, resId); } - return context.getString(R.string.spoken_description_unknown, code); + return resId; } } diff --git a/java/src/com/android/inputmethod/accessibility/KeyboardAccessibilityDelegate.java b/java/src/com/android/inputmethod/accessibility/KeyboardAccessibilityDelegate.java new file mode 100644 index 000000000..237117d10 --- /dev/null +++ b/java/src/com/android/inputmethod/accessibility/KeyboardAccessibilityDelegate.java @@ -0,0 +1,327 @@ +/* + * 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.accessibility; + +import android.content.Context; +import android.os.SystemClock; +import android.support.v4.view.AccessibilityDelegateCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; + +import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.keyboard.KeyDetector; +import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.keyboard.KeyboardView; + +/** + * This class represents a delegate that can be registered in a class that extends + * {@link KeyboardView} to enhance accessibility support via composition rather via inheritance. + * + * To implement accessibility mode, the target keyboard view has to:<p> + * - Call {@link #setKeyboard(Keyboard)} when a new keyboard is set to the keyboard view. + * - Dispatch a hover event by calling {@link #onHoverEnter(MotionEvent)}. + * + * @param <KV> The keyboard view class type. + */ +public class KeyboardAccessibilityDelegate<KV extends KeyboardView> + extends AccessibilityDelegateCompat { + private static final String TAG = KeyboardAccessibilityDelegate.class.getSimpleName(); + protected static final boolean DEBUG_HOVER = false; + + protected final KV mKeyboardView; + protected final KeyDetector mKeyDetector; + private Keyboard mKeyboard; + private KeyboardAccessibilityNodeProvider<KV> mAccessibilityNodeProvider; + private Key mLastHoverKey; + + public static final int HOVER_EVENT_POINTER_ID = 0; + + public KeyboardAccessibilityDelegate(final KV keyboardView, final KeyDetector keyDetector) { + super(); + mKeyboardView = keyboardView; + mKeyDetector = keyDetector; + + // Ensure that the view has an accessibility delegate. + ViewCompat.setAccessibilityDelegate(keyboardView, this); + } + + /** + * Called when the keyboard layout changes. + * <p> + * <b>Note:</b> This method will be called even if accessibility is not + * enabled. + * @param keyboard The keyboard that is being set to the wrapping view. + */ + public void setKeyboard(final Keyboard keyboard) { + if (keyboard == null) { + return; + } + if (mAccessibilityNodeProvider != null) { + mAccessibilityNodeProvider.setKeyboard(keyboard); + } + mKeyboard = keyboard; + } + + protected final Keyboard getKeyboard() { + return mKeyboard; + } + + protected final void setLastHoverKey(final Key key) { + mLastHoverKey = key; + } + + protected final Key getLastHoverKey() { + return mLastHoverKey; + } + + /** + * Sends a window state change event with the specified string resource id. + * + * @param resId The string resource id of the text to send with the event. + */ + protected void sendWindowStateChanged(final int resId) { + if (resId == 0) { + return; + } + final Context context = mKeyboardView.getContext(); + sendWindowStateChanged(context.getString(resId)); + } + + /** + * Sends a window state change event with the specified text. + * + * @param text The text to send with the event. + */ + protected void sendWindowStateChanged(final String text) { + final AccessibilityEvent stateChange = AccessibilityEvent.obtain( + AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + mKeyboardView.onInitializeAccessibilityEvent(stateChange); + stateChange.getText().add(text); + stateChange.setContentDescription(null); + + final ViewParent parent = mKeyboardView.getParent(); + if (parent != null) { + parent.requestSendAccessibilityEvent(mKeyboardView, stateChange); + } + } + + /** + * Delegate method for View.getAccessibilityNodeProvider(). This method is called in SDK + * version 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) and higher to obtain the virtual + * node hierarchy provider. + * + * @param host The host view for the provider. + * @return The accessibility node provider for the current keyboard. + */ + @Override + public KeyboardAccessibilityNodeProvider<KV> getAccessibilityNodeProvider(final View host) { + return getAccessibilityNodeProvider(); + } + + /** + * @return A lazily-instantiated node provider for this view delegate. + */ + protected KeyboardAccessibilityNodeProvider<KV> getAccessibilityNodeProvider() { + // Instantiate the provide only when requested. Since the system + // will call this method multiple times it is a good practice to + // cache the provider instance. + if (mAccessibilityNodeProvider == null) { + mAccessibilityNodeProvider = + new KeyboardAccessibilityNodeProvider<>(mKeyboardView, this); + } + return mAccessibilityNodeProvider; + } + + /** + * Get a key that a hover event is on. + * + * @param event The hover event. + * @return key The key that the <code>event</code> is on. + */ + protected final Key getHoverKeyOf(final MotionEvent event) { + final int actionIndex = event.getActionIndex(); + final int x = (int)event.getX(actionIndex); + final int y = (int)event.getY(actionIndex); + return mKeyDetector.detectHitKey(x, y); + } + + /** + * 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. + */ + public boolean onHoverEvent(final MotionEvent event) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_HOVER_ENTER: + onHoverEnter(event); + break; + case MotionEvent.ACTION_HOVER_MOVE: + onHoverMove(event); + break; + case MotionEvent.ACTION_HOVER_EXIT: + onHoverExit(event); + break; + default: + Log.w(getClass().getSimpleName(), "Unknown hover event: " + event); + break; + } + return true; + } + + /** + * Process {@link MotionEvent#ACTION_HOVER_ENTER} event. + * + * @param event A hover enter event. + */ + protected void onHoverEnter(final MotionEvent event) { + final Key key = getHoverKeyOf(event); + if (DEBUG_HOVER) { + Log.d(TAG, "onHoverEnter: key=" + key); + } + if (key != null) { + onHoverEnterTo(key); + } + setLastHoverKey(key); + } + + /** + * Process {@link MotionEvent#ACTION_HOVER_MOVE} event. + * + * @param event A hover move event. + */ + protected void onHoverMove(final MotionEvent event) { + final Key lastKey = getLastHoverKey(); + final Key key = getHoverKeyOf(event); + if (key != lastKey) { + if (lastKey != null) { + onHoverExitFrom(lastKey); + } + if (key != null) { + onHoverEnterTo(key); + } + } + if (key != null) { + onHoverMoveWithin(key); + } + setLastHoverKey(key); + } + + /** + * Process {@link MotionEvent#ACTION_HOVER_EXIT} event. + * + * @param event A hover exit event. + */ + protected void onHoverExit(final MotionEvent event) { + final Key lastKey = getLastHoverKey(); + if (DEBUG_HOVER) { + Log.d(TAG, "onHoverExit: key=" + getHoverKeyOf(event) + " last=" + lastKey); + } + if (lastKey != null) { + onHoverExitFrom(lastKey); + } + final Key key = getHoverKeyOf(event); + // Make sure we're not getting an EXIT event because the user slid + // off the keyboard area, then force a key press. + if (key != null) { + performClickOn(key); + onHoverExitFrom(key); + } + setLastHoverKey(null); + } + + /** + * Perform click on a key. + * + * @param key A key to be registered. + */ + public void performClickOn(final Key key) { + if (DEBUG_HOVER) { + Log.d(TAG, "performClickOn: key=" + key); + } + simulateTouchEvent(MotionEvent.ACTION_DOWN, key); + simulateTouchEvent(MotionEvent.ACTION_UP, key); + } + + /** + * Simulating a touch event by injecting a synthesized touch event into {@link KeyboardView}. + * + * @param touchAction The action of the synthesizing touch event. + * @param key The key that a synthesized touch event is on. + */ + private void simulateTouchEvent(final int touchAction, final Key key) { + final int x = key.getHitBox().centerX(); + final int y = key.getHitBox().centerY(); + final long eventTime = SystemClock.uptimeMillis(); + final MotionEvent touchEvent = MotionEvent.obtain( + eventTime, eventTime, touchAction, x, y, 0 /* metaState */); + mKeyboardView.onTouchEvent(touchEvent); + touchEvent.recycle(); + } + + /** + * Handles a hover enter event on a key. + * + * @param key The currently hovered key. + */ + protected void onHoverEnterTo(final Key key) { + if (DEBUG_HOVER) { + Log.d(TAG, "onHoverEnterTo: key=" + key); + } + key.onPressed(); + mKeyboardView.invalidateKey(key); + final KeyboardAccessibilityNodeProvider<KV> provider = getAccessibilityNodeProvider(); + provider.onHoverEnterTo(key); + provider.performActionForKey(key, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS); + } + + /** + * Handles a hover move event on a key. + * + * @param key The currently hovered key. + */ + protected void onHoverMoveWithin(final Key key) { } + + /** + * Handles a hover exit event on a key. + * + * @param key The currently hovered key. + */ + protected void onHoverExitFrom(final Key key) { + if (DEBUG_HOVER) { + Log.d(TAG, "onHoverExitFrom: key=" + key); + } + key.onReleased(); + mKeyboardView.invalidateKey(key); + final KeyboardAccessibilityNodeProvider<KV> provider = getAccessibilityNodeProvider(); + provider.onHoverExitFrom(key); + } + + /** + * Perform long click on a key. + * + * @param key A key to be long pressed on. + */ + public void performLongClickOn(final Key key) { + // A extended class should override this method to implement long press. + } +} diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java b/java/src/com/android/inputmethod/accessibility/KeyboardAccessibilityNodeProvider.java index c628c5b09..66b0acb2f 100644 --- a/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java +++ b/java/src/com/android/inputmethod/accessibility/KeyboardAccessibilityNodeProvider.java @@ -17,17 +17,13 @@ package com.android.inputmethod.accessibility; import android.graphics.Rect; -import android.inputmethodservice.InputMethodService; import android.os.Bundle; -import android.os.SystemClock; import android.support.v4.view.ViewCompat; import android.support.v4.view.accessibility.AccessibilityEventCompat; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat; import android.support.v4.view.accessibility.AccessibilityRecordCompat; import android.util.Log; -import android.util.SparseArray; -import android.view.MotionEvent; import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.view.inputmethod.EditorInfo; @@ -37,9 +33,10 @@ import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardView; import com.android.inputmethod.latin.settings.Settings; import com.android.inputmethod.latin.settings.SettingsValues; -import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.CoordinateUtils; +import java.util.List; + /** * Exposes a virtual view sub-tree for {@link KeyboardView} and generates * {@link AccessibilityEvent}s for individual {@link Key}s. @@ -50,17 +47,16 @@ import com.android.inputmethod.latin.utils.CoordinateUtils; * virtual views, thus conveying their logical structure. * </p> */ -public final class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat { - private static final String TAG = AccessibilityEntityProvider.class.getSimpleName(); - private static final int UNDEFINED = Integer.MIN_VALUE; +final class KeyboardAccessibilityNodeProvider<KV extends KeyboardView> + extends AccessibilityNodeProviderCompat { + private static final String TAG = KeyboardAccessibilityNodeProvider.class.getSimpleName(); + + // From {@link android.view.accessibility.AccessibilityNodeInfo#UNDEFINED_ITEM_ID}. + private static final int UNDEFINED = Integer.MAX_VALUE; - private final InputMethodService mInputMethodService; private final KeyCodeDescriptionMapper mKeyCodeDescriptionMapper; private final AccessibilityUtils mAccessibilityUtils; - /** A map of integer IDs to {@link Key}s. */ - private final SparseArray<Key> mVirtualViewIdToKey = CollectionUtils.newSparseArray(); - /** Temporary rect used to calculate in-screen bounds. */ private final Rect mTempBoundsInScreen = new Rect(); @@ -70,36 +66,64 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider /** The virtual view identifier for the focused node. */ private int mAccessibilityFocusedView = UNDEFINED; - /** The current keyboard view. */ - private KeyboardView mKeyboardView; + /** The virtual view identifier for the hovering node. */ + private int mHoveringNodeId = UNDEFINED; + + /** The keyboard view to provide an accessibility node info. */ + private final KV mKeyboardView; + /** The accessibility delegate. */ + private final KeyboardAccessibilityDelegate<KV> mDelegate; - public AccessibilityEntityProvider(final KeyboardView keyboardView, - final InputMethodService inputMethod) { - mInputMethodService = inputMethod; + /** The current keyboard. */ + private Keyboard mKeyboard; + + public KeyboardAccessibilityNodeProvider(final KV keyboardView, + final KeyboardAccessibilityDelegate<KV> delegate) { + super(); mKeyCodeDescriptionMapper = KeyCodeDescriptionMapper.getInstance(); mAccessibilityUtils = AccessibilityUtils.getInstance(); - setView(keyboardView); - } - - /** - * Sets the keyboard view represented by this node provider. - * - * @param keyboardView The keyboard view to represent. - */ - public void setView(final KeyboardView keyboardView) { mKeyboardView = keyboardView; - updateParentLocation(); + mDelegate = delegate; // Since this class is constructed lazily, we might not get a subsequent // call to setKeyboard() and therefore need to call it now. - setKeyboard(); + setKeyboard(keyboardView.getKeyboard()); } /** * Sets the keyboard represented by this node provider. + * + * @param keyboard The keyboard that is being set to the keyboard view. */ - public void setKeyboard() { - assignVirtualViewIds(); + public void setKeyboard(final Keyboard keyboard) { + mKeyboard = keyboard; + } + + private Key getKeyOf(final int virtualViewId) { + if (mKeyboard == null) { + return null; + } + final List<Key> sortedKeys = mKeyboard.getSortedKeys(); + // Use a virtual view id as an index of the sorted keys list. + if (virtualViewId >= 0 && virtualViewId < sortedKeys.size()) { + return sortedKeys.get(virtualViewId); + } + return null; + } + + private int getVirtualViewIdOf(final Key key) { + if (mKeyboard == null) { + return View.NO_ID; + } + final List<Key> sortedKeys = mKeyboard.getSortedKeys(); + final int size = sortedKeys.size(); + for (int index = 0; index < size; index++) { + if (sortedKeys.get(index) == key) { + // Use an index of the sorted keys list as a virtual view id. + return index; + } + } + return View.NO_ID; } /** @@ -112,18 +136,40 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider * @see AccessibilityEvent */ public AccessibilityEvent createAccessibilityEvent(final Key key, final int eventType) { - final int virtualViewId = generateVirtualViewIdForKey(key); + final int virtualViewId = getVirtualViewIdOf(key); final String keyDescription = getKeyDescription(key); final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); event.setPackageName(mKeyboardView.getContext().getPackageName()); event.setClassName(key.getClass().getName()); event.setContentDescription(keyDescription); event.setEnabled(true); - final AccessibilityRecordCompat record = new AccessibilityRecordCompat(event); + final AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event); record.setSource(mKeyboardView, virtualViewId); return event; } + public void onHoverEnterTo(final Key key) { + final int id = getVirtualViewIdOf(key); + if (id == View.NO_ID) { + return; + } + // Start hovering on the key. Because our accessibility model is lift-to-type, we should + // report the node info without click and long click actions to avoid unnecessary + // announcements. + mHoveringNodeId = id; + // Invalidate the node info of the key. + sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED); + sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER); + } + + public void onHoverExitFrom(final Key key) { + mHoveringNodeId = UNDEFINED; + // Invalidate the node info of the key to be able to revert the change we have done + // in {@link #onHoverEnterTo(Key)}. + sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED); + sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT); + } + /** * Returns an {@link AccessibilityNodeInfoCompat} representing a virtual * view, i.e. a descendant of the host View, with the given <code>virtualViewId</code> or @@ -156,19 +202,24 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider final AccessibilityNodeInfoCompat rootInfo = AccessibilityNodeInfoCompat.obtain(mKeyboardView); ViewCompat.onInitializeAccessibilityNodeInfo(mKeyboardView, rootInfo); + updateParentLocation(); // Add the virtual children of the root View. - final Keyboard keyboard = mKeyboardView.getKeyboard(); - final Key[] keys = keyboard.getKeys(); - for (Key key : keys) { - final int childVirtualViewId = generateVirtualViewIdForKey(key); - rootInfo.addChild(mKeyboardView, childVirtualViewId); + final List<Key> sortedKeys = mKeyboard.getSortedKeys(); + final int size = sortedKeys.size(); + for (int index = 0; index < size; index++) { + final Key key = sortedKeys.get(index); + if (key.isSpacer()) { + continue; + } + // Use an index of the sorted keys list as a virtual view id. + rootInfo.addChild(mKeyboardView, index); } return rootInfo; } - // Find the view that corresponds to the given id. - final Key key = mVirtualViewIdToKey.get(virtualViewId); + // Find the key that corresponds to the given virtual view id. + final Key key = getKeyOf(virtualViewId); if (key == null) { Log.e(TAG, "Invalid virtual view ID: " + virtualViewId); return null; @@ -191,9 +242,16 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider info.setBoundsInScreen(boundsInScreen); info.setParent(mKeyboardView); info.setSource(mKeyboardView, virtualViewId); - info.setBoundsInScreen(boundsInScreen); - info.setEnabled(true); + info.setEnabled(key.isEnabled()); info.setVisibleToUser(true); + // Don't add ACTION_CLICK and ACTION_LONG_CLOCK actions while hovering on the key. + // See {@link #onHoverEnterTo(Key)} and {@link #onHoverExitFrom(Key)}. + if (virtualViewId != mHoveringNodeId) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); + if (key.isLongPressEnabled()) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK); + } + } if (mAccessibilityFocusedView == virtualViewId) { info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS); @@ -203,35 +261,14 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider return info; } - /** - * Simulates a key press by injecting touch events into the keyboard view. - * This avoids the complexity of trackers and listeners within the keyboard. - * - * @param key The key to press. - */ - void simulateKeyPress(final Key key) { - final int x = key.getHitBox().centerX(); - final int y = key.getHitBox().centerY(); - final long downTime = SystemClock.uptimeMillis(); - final MotionEvent downEvent = MotionEvent.obtain( - downTime, downTime, MotionEvent.ACTION_DOWN, x, y, 0); - final MotionEvent upEvent = MotionEvent.obtain( - downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, x, y, 0); - - mKeyboardView.onTouchEvent(downEvent); - mKeyboardView.onTouchEvent(upEvent); - downEvent.recycle(); - upEvent.recycle(); - } - @Override public boolean performAction(final int virtualViewId, final int action, final Bundle arguments) { - final Key key = mVirtualViewIdToKey.get(virtualViewId); + final Key key = getKeyOf(virtualViewId); if (key == null) { return false; } - return performActionForKey(key, action, arguments); + return performActionForKey(key, action); } /** @@ -239,29 +276,28 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider * * @param key The on which to perform the action. * @param action The action to perform. - * @param arguments The action's arguments. * @return The result of performing the action, or false if the action is not supported. */ - boolean performActionForKey(final Key key, final int action, final Bundle arguments) { - final int virtualViewId = generateVirtualViewIdForKey(key); - + boolean performActionForKey(final Key key, final int action) { switch (action) { case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS: - if (mAccessibilityFocusedView == virtualViewId) { - return false; - } - mAccessibilityFocusedView = virtualViewId; + mAccessibilityFocusedView = getVirtualViewIdOf(key); sendAccessibilityEventForKey( key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED); return true; case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS: - if (mAccessibilityFocusedView != virtualViewId) { - return false; - } mAccessibilityFocusedView = UNDEFINED; sendAccessibilityEventForKey( key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); return true; + case AccessibilityNodeInfoCompat.ACTION_CLICK: + sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_VIEW_CLICKED); + mDelegate.performClickOn(key); + return true; + case AccessibilityNodeInfoCompat.ACTION_LONG_CLICK: + sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); + mDelegate.performLongClickOn(key); + return true; default: return false; } @@ -285,11 +321,11 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider * @return The context-specific description of the key. */ private String getKeyDescription(final Key key) { - final EditorInfo editorInfo = mInputMethodService.getCurrentInputEditorInfo(); + final EditorInfo editorInfo = mKeyboard.mId.mEditorInfo; final boolean shouldObscure = mAccessibilityUtils.shouldObscureInput(editorInfo); final SettingsValues currentSettings = Settings.getInstance().getCurrent(); final String keyCodeDescription = mKeyCodeDescriptionMapper.getDescriptionForKey( - mKeyboardView.getContext(), mKeyboardView.getKeyboard(), key, shouldObscure); + mKeyboardView.getContext(), mKeyboard, key, shouldObscure); if (currentSettings.isWordSeparator(key.getCode())) { return mAccessibilityUtils.getAutoCorrectionDescription( keyCodeDescription, shouldObscure); @@ -299,40 +335,9 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider } /** - * Assigns virtual view IDs to keyboard keys and populates the related maps. - */ - private void assignVirtualViewIds() { - final Keyboard keyboard = mKeyboardView.getKeyboard(); - if (keyboard == null) { - return; - } - mVirtualViewIdToKey.clear(); - - final Key[] keys = keyboard.getKeys(); - for (Key key : keys) { - final int virtualViewId = generateVirtualViewIdForKey(key); - mVirtualViewIdToKey.put(virtualViewId, key); - } - } - - /** * Updates the parent's on-screen location. */ private void updateParentLocation() { mKeyboardView.getLocationOnScreen(mParentLocation); } - - /** - * Generates a virtual view identifier for the given key. Returned - * identifiers are valid until the next global layout state change. - * - * @param key The key to identify. - * @return A virtual view identifier. - */ - private static int generateVirtualViewIdForKey(final Key key) { - // The key x- and y-coordinates are stable between layout changes. - // Generate an identifier by bit-shifting the x-coordinate to the - // left-half of the integer and OR'ing with the y-coordinate. - return ((0xFFFF & key.getX()) << (Integer.SIZE / 2)) | (0xFFFF & key.getY()); - } } diff --git a/java/src/com/android/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java b/java/src/com/android/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java new file mode 100644 index 000000000..b84d402fb --- /dev/null +++ b/java/src/com/android/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.accessibility; + +import android.content.Context; +import android.graphics.Rect; +import android.os.SystemClock; +import android.util.Log; +import android.util.SparseIntArray; +import android.view.MotionEvent; + +import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.keyboard.KeyDetector; +import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.keyboard.KeyboardId; +import com.android.inputmethod.keyboard.MainKeyboardView; +import com.android.inputmethod.keyboard.PointerTracker; +import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; + +/** + * This class represents a delegate that can be registered in {@link MainKeyboardView} to enhance + * accessibility support via composition rather via inheritance. + */ +public final class MainKeyboardAccessibilityDelegate + extends KeyboardAccessibilityDelegate<MainKeyboardView> + implements AccessibilityLongPressTimer.LongPressTimerCallback { + private static final String TAG = MainKeyboardAccessibilityDelegate.class.getSimpleName(); + + /** Map of keyboard modes to resource IDs. */ + private static final SparseIntArray KEYBOARD_MODE_RES_IDS = new SparseIntArray(); + + static { + KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_DATE, R.string.keyboard_mode_date); + KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_DATETIME, R.string.keyboard_mode_date_time); + KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_EMAIL, R.string.keyboard_mode_email); + KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_IM, R.string.keyboard_mode_im); + KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_NUMBER, R.string.keyboard_mode_number); + KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_PHONE, R.string.keyboard_mode_phone); + KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_TEXT, R.string.keyboard_mode_text); + KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_TIME, R.string.keyboard_mode_time); + KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_URL, R.string.keyboard_mode_url); + } + + /** The most recently set keyboard mode. */ + private int mLastKeyboardMode = KEYBOARD_IS_HIDDEN; + private static final int KEYBOARD_IS_HIDDEN = -1; + // The rectangle region to ignore hover events. + private final Rect mBoundsToIgnoreHoverEvent = new Rect(); + + private final AccessibilityLongPressTimer mAccessibilityLongPressTimer; + + public MainKeyboardAccessibilityDelegate(final MainKeyboardView mainKeyboardView, + final KeyDetector keyDetector) { + super(mainKeyboardView, keyDetector); + mAccessibilityLongPressTimer = new AccessibilityLongPressTimer( + this /* callback */, mainKeyboardView.getContext()); + } + + /** + * {@inheritDoc} + */ + @Override + public void setKeyboard(final Keyboard keyboard) { + if (keyboard == null) { + return; + } + final Keyboard lastKeyboard = getKeyboard(); + super.setKeyboard(keyboard); + final int lastKeyboardMode = mLastKeyboardMode; + mLastKeyboardMode = keyboard.mId.mMode; + + // Since this method is called even when accessibility is off, make sure + // to check the state before announcing anything. + if (!AccessibilityUtils.getInstance().isAccessibilityEnabled()) { + return; + } + // Announce the language name only when the language is changed. + if (lastKeyboard == null || !keyboard.mId.mSubtype.equals(lastKeyboard.mId.mSubtype)) { + announceKeyboardLanguage(keyboard); + return; + } + // Announce the mode only when the mode is changed. + if (keyboard.mId.mMode != lastKeyboardMode) { + announceKeyboardMode(keyboard); + return; + } + // Announce the keyboard type only when the type is changed. + if (keyboard.mId.mElementId != lastKeyboard.mId.mElementId) { + announceKeyboardType(keyboard, lastKeyboard); + return; + } + } + + /** + * Called when the keyboard is hidden and accessibility is enabled. + */ + public void onHideWindow() { + announceKeyboardHidden(); + mLastKeyboardMode = KEYBOARD_IS_HIDDEN; + } + + /** + * Announces which language of keyboard is being displayed. + * + * @param keyboard The new keyboard. + */ + private void announceKeyboardLanguage(final Keyboard keyboard) { + final String languageText = SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale( + keyboard.mId.mSubtype); + sendWindowStateChanged(languageText); + } + + /** + * Announces which type of keyboard is being displayed. + * If the keyboard type is unknown, no announcement is made. + * + * @param keyboard The new keyboard. + */ + private void announceKeyboardMode(final Keyboard keyboard) { + final Context context = mKeyboardView.getContext(); + final int modeTextResId = KEYBOARD_MODE_RES_IDS.get(keyboard.mId.mMode); + if (modeTextResId == 0) { + return; + } + final String modeText = context.getString(modeTextResId); + final String text = context.getString(R.string.announce_keyboard_mode, modeText); + sendWindowStateChanged(text); + } + + /** + * Announces which type of keyboard is being displayed. + * + * @param keyboard The new keyboard. + * @param lastKeyboard The last keyboard. + */ + private void announceKeyboardType(final Keyboard keyboard, final Keyboard lastKeyboard) { + final int lastElementId = lastKeyboard.mId.mElementId; + final int resId; + switch (keyboard.mId.mElementId) { + case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: + case KeyboardId.ELEMENT_ALPHABET: + if (lastElementId == KeyboardId.ELEMENT_ALPHABET + || lastElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) { + // Transition between alphabet mode and automatic shifted mode should be silently + // ignored because it can be determined by each key's talk back announce. + return; + } + resId = R.string.spoken_description_mode_alpha; + break; + case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: + if (lastElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) { + // Resetting automatic shifted mode by pressing the shift key causes the transition + // from automatic shifted to manual shifted that should be silently ignored. + return; + } + resId = R.string.spoken_description_shiftmode_on; + break; + case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: + if (lastElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED) { + // Resetting caps locked mode by pressing the shift key causes the transition + // from shift locked to shift lock shifted that should be silently ignored. + return; + } + resId = R.string.spoken_description_shiftmode_locked; + break; + case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED: + resId = R.string.spoken_description_shiftmode_locked; + break; + case KeyboardId.ELEMENT_SYMBOLS: + resId = R.string.spoken_description_mode_symbol; + break; + case KeyboardId.ELEMENT_SYMBOLS_SHIFTED: + resId = R.string.spoken_description_mode_symbol_shift; + break; + case KeyboardId.ELEMENT_PHONE: + resId = R.string.spoken_description_mode_phone; + break; + case KeyboardId.ELEMENT_PHONE_SYMBOLS: + resId = R.string.spoken_description_mode_phone_shift; + break; + default: + return; + } + sendWindowStateChanged(resId); + } + + /** + * Announces that the keyboard has been hidden. + */ + private void announceKeyboardHidden() { + sendWindowStateChanged(R.string.announce_keyboard_hidden); + } + + @Override + public void performClickOn(final Key key) { + final int x = key.getHitBox().centerX(); + final int y = key.getHitBox().centerY(); + if (DEBUG_HOVER) { + Log.d(TAG, "performClickOn: key=" + key + + " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y)); + } + if (mBoundsToIgnoreHoverEvent.contains(x, y)) { + // This hover exit event points to the key that should be ignored. + // Clear the ignoring region to handle further hover events. + mBoundsToIgnoreHoverEvent.setEmpty(); + return; + } + super.performClickOn(key); + } + + @Override + protected void onHoverEnterTo(final Key key) { + final int x = key.getHitBox().centerX(); + final int y = key.getHitBox().centerY(); + if (DEBUG_HOVER) { + Log.d(TAG, "onHoverEnterTo: key=" + key + + " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y)); + } + mAccessibilityLongPressTimer.cancelLongPress(); + if (mBoundsToIgnoreHoverEvent.contains(x, y)) { + return; + } + // This hover enter event points to the key that isn't in the ignoring region. + // Further hover events should be handled. + mBoundsToIgnoreHoverEvent.setEmpty(); + super.onHoverEnterTo(key); + if (key.isLongPressEnabled()) { + mAccessibilityLongPressTimer.startLongPress(key); + } + } + + @Override + protected void onHoverExitFrom(final Key key) { + final int x = key.getHitBox().centerX(); + final int y = key.getHitBox().centerY(); + if (DEBUG_HOVER) { + Log.d(TAG, "onHoverExitFrom: key=" + key + + " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y)); + } + mAccessibilityLongPressTimer.cancelLongPress(); + super.onHoverExitFrom(key); + } + + @Override + public void performLongClickOn(final Key key) { + if (DEBUG_HOVER) { + Log.d(TAG, "performLongClickOn: key=" + key); + } + final PointerTracker tracker = PointerTracker.getPointerTracker(HOVER_EVENT_POINTER_ID); + final long eventTime = SystemClock.uptimeMillis(); + final int x = key.getHitBox().centerX(); + final int y = key.getHitBox().centerY(); + final MotionEvent downEvent = MotionEvent.obtain( + eventTime, eventTime, MotionEvent.ACTION_DOWN, x, y, 0 /* metaState */); + // Inject a fake down event to {@link PointerTracker} to handle a long press correctly. + tracker.processMotionEvent(downEvent, mKeyDetector); + // The above fake down event triggers an unnecessary long press timer that should be + // canceled. + tracker.cancelLongPressTimer(); + downEvent.recycle(); + // Invoke {@link MainKeyboardView#onLongPress(PointerTracker)} as if a long press timeout + // has passed. + mKeyboardView.onLongPress(tracker); + // If {@link Key#hasNoPanelAutoMoreKeys()} is true (such as "0 +" key on the phone layout) + // or a key invokes IME switcher dialog, we should just ignore the next + // {@link #onRegisterHoverKey(Key,MotionEvent)}. It can be determined by whether + // {@link PointerTracker} is in operation or not. + if (tracker.isInOperation()) { + // This long press shows a more keys keyboard and further hover events should be + // handled. + mBoundsToIgnoreHoverEvent.setEmpty(); + return; + } + // This long press has handled at {@link MainKeyboardView#onLongPress(PointerTracker)}. + // We should ignore further hover events on this key. + mBoundsToIgnoreHoverEvent.set(key.getHitBox()); + if (key.hasNoPanelAutoMoreKey()) { + // This long press has registered a code point without showing a more keys keyboard. + // We should talk back the code point if possible. + final int codePointOfNoPanelAutoMoreKey = key.getMoreKeys()[0].mCode; + final String text = KeyCodeDescriptionMapper.getInstance().getDescriptionForCodePoint( + mKeyboardView.getContext(), codePointOfNoPanelAutoMoreKey); + if (text != null) { + sendWindowStateChanged(text); + } + } + } +} diff --git a/java/src/com/android/inputmethod/accessibility/MoreKeysKeyboardAccessibilityDelegate.java b/java/src/com/android/inputmethod/accessibility/MoreKeysKeyboardAccessibilityDelegate.java new file mode 100644 index 000000000..4022da343 --- /dev/null +++ b/java/src/com/android/inputmethod/accessibility/MoreKeysKeyboardAccessibilityDelegate.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.accessibility; + +import android.graphics.Rect; +import android.util.Log; +import android.view.MotionEvent; + +import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.keyboard.KeyDetector; +import com.android.inputmethod.keyboard.MoreKeysKeyboardView; +import com.android.inputmethod.keyboard.PointerTracker; + +/** + * This class represents a delegate that can be registered in {@link MoreKeysKeyboardView} to + * enhance accessibility support via composition rather via inheritance. + */ +public class MoreKeysKeyboardAccessibilityDelegate + extends KeyboardAccessibilityDelegate<MoreKeysKeyboardView> { + private static final String TAG = MoreKeysKeyboardAccessibilityDelegate.class.getSimpleName(); + + private final Rect mMoreKeysKeyboardValidBounds = new Rect(); + private static final int CLOSING_INSET_IN_PIXEL = 1; + private int mOpenAnnounceResId; + private int mCloseAnnounceResId; + + public MoreKeysKeyboardAccessibilityDelegate(final MoreKeysKeyboardView moreKeysKeyboardView, + final KeyDetector keyDetector) { + super(moreKeysKeyboardView, keyDetector); + } + + public void setOpenAnnounce(final int resId) { + mOpenAnnounceResId = resId; + } + + public void setCloseAnnounce(final int resId) { + mCloseAnnounceResId = resId; + } + + public void onShowMoreKeysKeyboard() { + sendWindowStateChanged(mOpenAnnounceResId); + } + + public void onDismissMoreKeysKeyboard() { + sendWindowStateChanged(mCloseAnnounceResId); + } + + @Override + protected void onHoverEnter(final MotionEvent event) { + if (DEBUG_HOVER) { + Log.d(TAG, "onHoverEnter: key=" + getHoverKeyOf(event)); + } + super.onHoverEnter(event); + final int actionIndex = event.getActionIndex(); + final int x = (int)event.getX(actionIndex); + final int y = (int)event.getY(actionIndex); + final int pointerId = event.getPointerId(actionIndex); + final long eventTime = event.getEventTime(); + mKeyboardView.onDownEvent(x, y, pointerId, eventTime); + } + + @Override + protected void onHoverMove(final MotionEvent event) { + super.onHoverMove(event); + final int actionIndex = event.getActionIndex(); + final int x = (int)event.getX(actionIndex); + final int y = (int)event.getY(actionIndex); + final int pointerId = event.getPointerId(actionIndex); + final long eventTime = event.getEventTime(); + mKeyboardView.onMoveEvent(x, y, pointerId, eventTime); + } + + @Override + protected void onHoverExit(final MotionEvent event) { + final Key lastKey = getLastHoverKey(); + if (DEBUG_HOVER) { + Log.d(TAG, "onHoverExit: key=" + getHoverKeyOf(event) + " last=" + lastKey); + } + if (lastKey != null) { + super.onHoverExitFrom(lastKey); + } + setLastHoverKey(null); + final int actionIndex = event.getActionIndex(); + final int x = (int)event.getX(actionIndex); + final int y = (int)event.getY(actionIndex); + final int pointerId = event.getPointerId(actionIndex); + final long eventTime = event.getEventTime(); + // A hover exit event at one pixel width or height area on the edges of more keys keyboard + // are treated as closing. + mMoreKeysKeyboardValidBounds.set(0, 0, mKeyboardView.getWidth(), mKeyboardView.getHeight()); + mMoreKeysKeyboardValidBounds.inset(CLOSING_INSET_IN_PIXEL, CLOSING_INSET_IN_PIXEL); + if (mMoreKeysKeyboardValidBounds.contains(x, y)) { + // Invoke {@link MoreKeysKeyboardView#onUpEvent(int,int,int,long)} as if this hover + // exit event selects a key. + mKeyboardView.onUpEvent(x, y, pointerId, eventTime); + // TODO: Should fix this reference. This is a hack to clear the state of + // {@link PointerTracker}. + PointerTracker.dismissAllMoreKeysPanels(); + return; + } + // Close the more keys keyboard. + // TODO: Should fix this reference. This is a hack to clear the state of + // {@link PointerTracker}. + PointerTracker.dismissAllMoreKeysPanels(); + } +} diff --git a/java/src/com/android/inputmethod/compat/AppWorkaroundsUtils.java b/java/src/com/android/inputmethod/compat/AppWorkaroundsUtils.java index 7e9e2e37b..6e43cc9a7 100644 --- a/java/src/com/android/inputmethod/compat/AppWorkaroundsUtils.java +++ b/java/src/com/android/inputmethod/compat/AppWorkaroundsUtils.java @@ -23,10 +23,10 @@ import android.os.Build.VERSION_CODES; * A class to encapsulate work-arounds specific to particular apps. */ public class AppWorkaroundsUtils { - private PackageInfo mPackageInfo; // May be null - private boolean mIsBrokenByRecorrection = false; + private final PackageInfo mPackageInfo; // May be null + private final boolean mIsBrokenByRecorrection; - public void setPackageInfo(final PackageInfo packageInfo) { + public AppWorkaroundsUtils(final PackageInfo packageInfo) { mPackageInfo = packageInfo; mIsBrokenByRecorrection = AppWorkaroundsHelper.evaluateIsBrokenByRecorrection( packageInfo); diff --git a/java/src/com/android/inputmethod/compat/InputMethodManagerCompatWrapper.java b/java/src/com/android/inputmethod/compat/InputMethodManagerCompatWrapper.java index a80c3fefe..18b3a6060 100644 --- a/java/src/com/android/inputmethod/compat/InputMethodManagerCompatWrapper.java +++ b/java/src/com/android/inputmethod/compat/InputMethodManagerCompatWrapper.java @@ -28,6 +28,12 @@ public final class InputMethodManagerCompatWrapper { private static final Method METHOD_switchToNextInputMethod = CompatUtils.getMethod( InputMethodManager.class, "switchToNextInputMethod", IBinder.class, Boolean.TYPE); + // Note that InputMethodManager.shouldOfferSwitchingToNextInputMethod() has been introduced + // in API level 19 (Build.VERSION_CODES.KITKAT). + private static final Method METHOD_shouldOfferSwitchingToNextInputMethod = + CompatUtils.getMethod(InputMethodManager.class, + "shouldOfferSwitchingToNextInputMethod", IBinder.class); + public final InputMethodManager mImm; public InputMethodManagerCompatWrapper(final Context context) { @@ -38,4 +44,9 @@ public final class InputMethodManagerCompatWrapper { return (Boolean)CompatUtils.invoke(mImm, false /* defaultValue */, METHOD_switchToNextInputMethod, token, onlyCurrentIme); } + + public boolean shouldOfferSwitchingToNextInputMethod(final IBinder token) { + return (Boolean)CompatUtils.invoke(mImm, false /* defaultValue */, + METHOD_shouldOfferSwitchingToNextInputMethod, token); + } } diff --git a/java/src/com/android/inputmethod/compat/InputMethodServiceCompatUtils.java b/java/src/com/android/inputmethod/compat/InputMethodServiceCompatUtils.java index 14ee654f3..81df17127 100644 --- a/java/src/com/android/inputmethod/compat/InputMethodServiceCompatUtils.java +++ b/java/src/com/android/inputmethod/compat/InputMethodServiceCompatUtils.java @@ -17,11 +17,12 @@ package com.android.inputmethod.compat; import android.inputmethodservice.InputMethodService; +import com.android.inputmethod.latin.define.ProductionFlag; import java.lang.reflect.Method; public final class InputMethodServiceCompatUtils { - // Note that InputMethodService.enableHardwareAcceleration() has been introduced + // Note that {@link InputMethodService#enableHardwareAcceleration} has been introduced // in API level 17 (Build.VERSION_CODES.JELLY_BEAN_MR1). private static final Method METHOD_enableHardwareAcceleration = CompatUtils.getMethod(InputMethodService.class, "enableHardwareAcceleration"); @@ -34,4 +35,30 @@ public final class InputMethodServiceCompatUtils { return (Boolean)CompatUtils.invoke(ims, false /* defaultValue */, METHOD_enableHardwareAcceleration); } + + public static void setCursorAnchorMonitorMode(final InputMethodService ims, final int mode) { + if (ProductionFlag.USES_CURSOR_ANCHOR_MONITOR) { + ExperimentalAPIUtils.setCursorAnchorMonitorMode(ims, mode); + } + } + + /* + * For unreleased APIs. ProGuard will strip this class entirely, unless used explicitly. + */ + private static final class ExperimentalAPIUtils { + // Note that {@link InputMethodManager#setCursorAnchorMonitorMode} is not yet available as + // an official API as of API level 19 (Build.VERSION_CODES.KITKAT). + private static final Method METHOD_setCursorAnchorMonitorMode = CompatUtils.getMethod( + InputMethodService.class, "setCursorAnchorMonitorMode", int.class); + + private ExperimentalAPIUtils() { + // This utility class is not publicly instantiable. + } + + public static void setCursorAnchorMonitorMode(final InputMethodService ims, + final int mode) { + CompatUtils.invoke(ims, null /* defaultValue */, + METHOD_setCursorAnchorMonitorMode, mode); + } + } } diff --git a/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java b/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java index b119d6c82..ee9125a07 100644 --- a/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java +++ b/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java @@ -19,7 +19,11 @@ package com.android.inputmethod.compat; import android.os.Build; import android.view.inputmethod.InputMethodSubtype; +import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.Constants; + import java.lang.reflect.Constructor; +import java.lang.reflect.Method; public final class InputMethodSubtypeCompatUtils { private static final String TAG = InputMethodSubtypeCompatUtils.class.getSimpleName(); @@ -37,6 +41,12 @@ public final class InputMethodSubtypeCompatUtils { } } } + + // Note that {@link InputMethodSubtype#isAsciiCapable()} has been introduced in API level 19 + // (Build.VERSION_CODE.KITKAT). + private static final Method METHOD_isAsciiCapable = CompatUtils.getMethod( + InputMethodSubtype.class, "isAsciiCapable"); + private InputMethodSubtypeCompatUtils() { // This utility class is not publicly instantiable. } @@ -53,4 +63,14 @@ public final class InputMethodSubtypeCompatUtils { nameId, iconId, locale, mode, extraValue, isAuxiliary, overridesImplicitlyEnabledSubtype, id); } + + public static boolean isAsciiCapable(final InputMethodSubtype subtype) { + return isAsciiCapableWithAPI(subtype) + || subtype.containsExtraValueKey(Constants.Subtype.ExtraValue.ASCII_CAPABLE); + } + + @UsedForTesting + public static boolean isAsciiCapableWithAPI(final InputMethodSubtype subtype) { + return (Boolean)CompatUtils.invoke(subtype, false, METHOD_isAsciiCapable); + } } diff --git a/java/src/com/android/inputmethod/compat/LocaleSpanCompatUtils.java b/java/src/com/android/inputmethod/compat/LocaleSpanCompatUtils.java new file mode 100644 index 000000000..f411f181b --- /dev/null +++ b/java/src/com/android/inputmethod/compat/LocaleSpanCompatUtils.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.compat; + +import android.text.Spannable; +import android.text.style.LocaleSpan; +import android.util.Log; + +import com.android.inputmethod.annotations.UsedForTesting; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Locale; + +@UsedForTesting +public final class LocaleSpanCompatUtils { + private static final String TAG = LocaleSpanCompatUtils.class.getSimpleName(); + + // Note that LocaleSpan(Locale locale) has been introduced in API level 17 + // (Build.VERSION_CODE.JELLY_BEAN_MR1). + private static Class<?> getLocaleSpanClass() { + try { + return Class.forName("android.text.style.LocaleSpan"); + } catch (ClassNotFoundException e) { + return null; + } + } + private static final Class<?> LOCALE_SPAN_TYPE; + private static final Constructor<?> LOCALE_SPAN_CONSTRUCTOR; + private static final Method LOCALE_SPAN_GET_LOCALE; + static { + LOCALE_SPAN_TYPE = getLocaleSpanClass(); + LOCALE_SPAN_CONSTRUCTOR = CompatUtils.getConstructor(LOCALE_SPAN_TYPE, Locale.class); + LOCALE_SPAN_GET_LOCALE = CompatUtils.getMethod(LOCALE_SPAN_TYPE, "getLocale"); + } + + @UsedForTesting + public static boolean isLocaleSpanAvailable() { + return (LOCALE_SPAN_CONSTRUCTOR != null && LOCALE_SPAN_GET_LOCALE != null); + } + + @UsedForTesting + public static Object newLocaleSpan(final Locale locale) { + return CompatUtils.newInstance(LOCALE_SPAN_CONSTRUCTOR, locale); + } + + @UsedForTesting + public static Locale getLocaleFromLocaleSpan(final Object localeSpan) { + return (Locale) CompatUtils.invoke(localeSpan, null, LOCALE_SPAN_GET_LOCALE); + } + + /** + * Ensures that the specified range is covered with only one {@link LocaleSpan} with the given + * locale. If the region is already covered by one or more {@link LocaleSpan}, their ranges are + * updated so that each character has only one locale. + * @param spannable the spannable object to be updated. + * @param start the start index from which {@link LocaleSpan} is attached (inclusive). + * @param end the end index to which {@link LocaleSpan} is attached (exclusive). + * @param locale the locale to be attached to the specified range. + */ + @UsedForTesting + public static void updateLocaleSpan(final Spannable spannable, final int start, + final int end, final Locale locale) { + if (end < start) { + Log.e(TAG, "Invalid range: start=" + start + " end=" + end); + return; + } + if (!isLocaleSpanAvailable()) { + return; + } + // A brief summary of our strategy; + // 1. Enumerate all LocaleSpans between [start - 1, end + 1]. + // 2. For each LocaleSpan S: + // - Update the range of S so as not to cover [start, end] if S doesn't have the + // expected locale. + // - Mark S as "to be merged" if S has the expected locale. + // 3. Merge all the LocaleSpans that are marked as "to be merged" into one LocaleSpan. + // If no appropriate span is found, create a new one with newLocaleSpan method. + final int searchStart = Math.max(start - 1, 0); + final int searchEnd = Math.min(end + 1, spannable.length()); + // LocaleSpans found in the target range. See the step 1 in the above comment. + final Object[] existingLocaleSpans = spannable.getSpans(searchStart, searchEnd, + LOCALE_SPAN_TYPE); + // LocaleSpans that are marked as "to be merged". See the step 2 in the above comment. + final ArrayList<Object> existingLocaleSpansToBeMerged = new ArrayList<>(); + boolean isStartExclusive = true; + boolean isEndExclusive = true; + int newStart = start; + int newEnd = end; + for (final Object existingLocaleSpan : existingLocaleSpans) { + final Locale attachedLocale = getLocaleFromLocaleSpan(existingLocaleSpan); + if (!locale.equals(attachedLocale)) { + // This LocaleSpan does not have the expected locale. Update its range if it has + // an intersection with the range [start, end] (the first case of the step 2 in the + // above comment). + removeLocaleSpanFromRange(existingLocaleSpan, spannable, start, end); + continue; + } + final int spanStart = spannable.getSpanStart(existingLocaleSpan); + final int spanEnd = spannable.getSpanEnd(existingLocaleSpan); + if (spanEnd < spanStart) { + Log.e(TAG, "Invalid span: spanStart=" + spanStart + " spanEnd=" + spanEnd); + continue; + } + if (spanEnd < start || end < spanStart) { + // No intersection found. + continue; + } + + // Here existingLocaleSpan has the expected locale and an intersection with the + // range [start, end] (the second case of the the step 2 in the above comment). + final int spanFlag = spannable.getSpanFlags(existingLocaleSpan); + if (spanStart < newStart) { + newStart = spanStart; + isStartExclusive = ((spanFlag & Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) == + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + if (newEnd < spanEnd) { + newEnd = spanEnd; + isEndExclusive = ((spanFlag & Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) == + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + existingLocaleSpansToBeMerged.add(existingLocaleSpan); + } + + int originalLocaleSpanFlag = 0; + Object localeSpan = null; + if (existingLocaleSpansToBeMerged.isEmpty()) { + // If there is no LocaleSpan that is marked as to be merged, create a new one. + localeSpan = newLocaleSpan(locale); + } else { + // Reuse the first LocaleSpan to avoid unnecessary object instantiation. + localeSpan = existingLocaleSpansToBeMerged.get(0); + originalLocaleSpanFlag = spannable.getSpanFlags(localeSpan); + // No need to keep other instances. + for (int i = 1; i < existingLocaleSpansToBeMerged.size(); ++i) { + spannable.removeSpan(existingLocaleSpansToBeMerged.get(i)); + } + } + final int localeSpanFlag = getSpanFlag(originalLocaleSpanFlag, isStartExclusive, + isEndExclusive); + spannable.setSpan(localeSpan, newStart, newEnd, localeSpanFlag); + } + + private static void removeLocaleSpanFromRange(final Object localeSpan, + final Spannable spannable, final int removeStart, final int removeEnd) { + if (!isLocaleSpanAvailable()) { + return; + } + final int spanStart = spannable.getSpanStart(localeSpan); + final int spanEnd = spannable.getSpanEnd(localeSpan); + if (spanStart > spanEnd) { + Log.e(TAG, "Invalid span: spanStart=" + spanStart + " spanEnd=" + spanEnd); + return; + } + if (spanEnd < removeStart) { + // spanStart < spanEnd < removeStart < removeEnd + return; + } + if (removeEnd < spanStart) { + // spanStart < removeEnd < spanStart < spanEnd + return; + } + final int spanFlags = spannable.getSpanFlags(localeSpan); + if (spanStart < removeStart) { + if (removeEnd < spanEnd) { + // spanStart < removeStart < removeEnd < spanEnd + final Locale locale = getLocaleFromLocaleSpan(localeSpan); + spannable.setSpan(localeSpan, spanStart, removeStart, spanFlags); + final Object attionalLocaleSpan = newLocaleSpan(locale); + spannable.setSpan(attionalLocaleSpan, removeEnd, spanEnd, spanFlags); + return; + } + // spanStart < removeStart < spanEnd <= removeEnd + spannable.setSpan(localeSpan, spanStart, removeStart, spanFlags); + return; + } + if (removeEnd < spanEnd) { + // removeStart <= spanStart < removeEnd < spanEnd + spannable.setSpan(localeSpan, removeEnd, spanEnd, spanFlags); + return; + } + // removeStart <= spanStart < spanEnd < removeEnd + spannable.removeSpan(localeSpan); + } + + private static int getSpanFlag(final int originalFlag, + final boolean isStartExclusive, final boolean isEndExclusive) { + return (originalFlag & ~Spannable.SPAN_POINT_MARK_MASK) | + getSpanPointMarkFlag(isStartExclusive, isEndExclusive); + } + + private static int getSpanPointMarkFlag(final boolean isStartExclusive, + final boolean isEndExclusive) { + if (isStartExclusive) { + if (isEndExclusive) { + return Spannable.SPAN_EXCLUSIVE_EXCLUSIVE; + } else { + return Spannable.SPAN_EXCLUSIVE_INCLUSIVE; + } + } else { + if (isEndExclusive) { + return Spannable.SPAN_INCLUSIVE_EXCLUSIVE; + } else { + return Spannable.SPAN_INCLUSIVE_INCLUSIVE; + } + } + } +} diff --git a/java/src/com/android/inputmethod/compat/LooperCompatUtils.java b/java/src/com/android/inputmethod/compat/LooperCompatUtils.java new file mode 100644 index 000000000..d647dbbd3 --- /dev/null +++ b/java/src/com/android/inputmethod/compat/LooperCompatUtils.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.compat; + +import android.os.Looper; + +import java.lang.reflect.Method; + +/** + * Helper to call Looper#quitSafely, which was introduced in API + * level 18 (Build.VERSION_CODES.JELLY_BEAN_MR2). + * + * In unit tests, we create lots of instances of LatinIME, which means we need to clean up + * some Loopers lest we leak file descriptors. In normal use on a device though, this is never + * necessary (although it does not hurt). + */ +public final class LooperCompatUtils { + private static final Method METHOD_quitSafely = CompatUtils.getMethod( + Looper.class, "quitSafely"); + + public static void quitSafely(final Looper looper) { + if (null != METHOD_quitSafely) { + CompatUtils.invoke(looper, null /* default return value */, METHOD_quitSafely); + } else { + looper.quit(); + } + } +} diff --git a/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java b/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java index 55282c583..4d51821f2 100644 --- a/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java +++ b/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java @@ -25,8 +25,8 @@ import android.text.style.SuggestionSpan; import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.SuggestedWords; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.SuggestionSpanPickedNotificationReceiver; -import com.android.inputmethod.latin.utils.CollectionUtils; import java.lang.reflect.Field; import java.util.ArrayList; @@ -66,30 +66,30 @@ public final class SuggestionSpanUtils { } public static CharSequence getTextWithSuggestionSpan(final Context context, - final String pickedWord, final SuggestedWords suggestedWords, - final boolean dictionaryAvailable) { - if (!dictionaryAvailable || TextUtils.isEmpty(pickedWord) || suggestedWords.isEmpty() - || suggestedWords.mIsPrediction || suggestedWords.mIsPunctuationSuggestions) { + final String pickedWord, final SuggestedWords suggestedWords) { + if (TextUtils.isEmpty(pickedWord) || suggestedWords.isEmpty() + || suggestedWords.mIsPrediction || suggestedWords.isPunctuationSuggestions()) { return pickedWord; } - final Spannable spannable = new SpannableString(pickedWord); - final ArrayList<String> suggestionsList = CollectionUtils.newArrayList(); + final ArrayList<String> suggestionsList = new ArrayList<>(); for (int i = 0; i < suggestedWords.size(); ++i) { if (suggestionsList.size() >= SuggestionSpan.SUGGESTIONS_MAX_SIZE) { break; } + final SuggestedWordInfo info = suggestedWords.getInfo(i); + if (info.isKindOf(SuggestedWordInfo.KIND_PREDICTION)) { + continue; + } final String word = suggestedWords.getWord(i); if (!TextUtils.equals(pickedWord, word)) { suggestionsList.add(word.toString()); } } - - // TODO: We should avoid adding suggestion span candidates that came from the bigram - // prediction. final SuggestionSpan suggestionSpan = new SuggestionSpan(context, null /* locale */, suggestionsList.toArray(new String[suggestionsList.size()]), 0 /* flags */, SuggestionSpanPickedNotificationReceiver.class); + final Spannable spannable = new SpannableString(pickedWord); spannable.setSpan(suggestionSpan, 0, pickedWord.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); return spannable; } diff --git a/java/src/com/android/inputmethod/compat/UserDictionaryCompatUtils.java b/java/src/com/android/inputmethod/compat/UserDictionaryCompatUtils.java index a0d76415c..6e32e74ab 100644 --- a/java/src/com/android/inputmethod/compat/UserDictionaryCompatUtils.java +++ b/java/src/com/android/inputmethod/compat/UserDictionaryCompatUtils.java @@ -29,8 +29,8 @@ public final class UserDictionaryCompatUtils { Context.class, String.class, Integer.TYPE, String.class, Locale.class); @SuppressWarnings("deprecation") - public static void addWord(final Context context, final String word, final int freq, - final String shortcut, final Locale locale) { + public static void addWord(final Context context, final String word, + final int freq, final String shortcut, final Locale locale) { if (hasNewerAddWord()) { CompatUtils.invoke(Words.class, null, METHOD_addWord, context, word, freq, shortcut, locale); diff --git a/java/src/com/android/inputmethod/compat/ViewCompatUtils.java b/java/src/com/android/inputmethod/compat/ViewCompatUtils.java index a8fab8855..767cc423d 100644 --- a/java/src/com/android/inputmethod/compat/ViewCompatUtils.java +++ b/java/src/com/android/inputmethod/compat/ViewCompatUtils.java @@ -20,24 +20,17 @@ import android.view.View; import java.lang.reflect.Method; +// TODO: Use {@link android.support.v4.view.ViewCompat} instead of this utility class. +// Currently {@link #getPaddingEnd(View)} and {@link #setPaddingRelative(View,int,int,int,int)} +// are missing from android-support-v4 static library in KitKat SDK. public final class ViewCompatUtils { - // Note that View.LAYOUT_DIRECTION_LTR and View.LAYOUT_DIRECTION_RTL have been introduced in - // API level 17 (Build.VERSION_CODE.JELLY_BEAN_MR1). - public static final int LAYOUT_DIRECTION_LTR = (Integer)CompatUtils.getFieldValue(null, 0x0, - CompatUtils.getField(View.class, "LAYOUT_DIRECTION_LTR")); - public static final int LAYOUT_DIRECTION_RTL = (Integer)CompatUtils.getFieldValue(null, 0x1, - CompatUtils.getField(View.class, "LAYOUT_DIRECTION_RTL")); - - // Note that View.getPaddingEnd(), View.setPaddingRelative(int,int,int,int), and - // View.getLayoutDirection() have been introduced in API level 17 - // (Build.VERSION_CODE.JELLY_BEAN_MR1). + // Note that View.getPaddingEnd(), View.setPaddingRelative(int,int,int,int) have been + // introduced in API level 17 (Build.VERSION_CODE.JELLY_BEAN_MR1). private static final Method METHOD_getPaddingEnd = CompatUtils.getMethod( View.class, "getPaddingEnd"); private static final Method METHOD_setPaddingRelative = CompatUtils.getMethod( View.class, "setPaddingRelative", Integer.TYPE, Integer.TYPE, Integer.TYPE, Integer.TYPE); - private static final Method METHOD_getLayoutDirection = CompatUtils.getMethod( - View.class, "getLayoutDirection"); private ViewCompatUtils() { // This utility class is not publicly instantiable. @@ -58,11 +51,4 @@ public final class ViewCompatUtils { } CompatUtils.invoke(view, null, METHOD_setPaddingRelative, start, top, end, bottom); } - - public static int getLayoutDirection(final View view) { - if (METHOD_getLayoutDirection == null) { - return LAYOUT_DIRECTION_LTR; - } - return (Integer)CompatUtils.invoke(view, 0, METHOD_getLayoutDirection); - } } diff --git a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java index d5e638e7e..3d294acd7 100644 --- a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java +++ b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java @@ -16,7 +16,6 @@ package com.android.inputmethod.dictionarypack; -import android.app.DownloadManager; import android.app.DownloadManager.Request; import android.content.ContentValues; import android.content.Context; @@ -117,16 +116,11 @@ public final class ActionBatch { final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db, mWordList.mId, mWordList.mVersion); final int status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN); - final DownloadManager manager = - (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); if (MetadataDbHelper.STATUS_DOWNLOADING == status) { // The word list is still downloading. Cancel the download and revert the // word list status to "available". - if (null != manager) { - // DownloadManager is disabled (or not installed?). We can't cancel - there - // is nothing we can do. We still need to mark the entry as available. - manager.remove(values.getAsLong(MetadataDbHelper.PENDINGID_COLUMN)); - } + manager.remove(values.getAsLong(MetadataDbHelper.PENDINGID_COLUMN)); MetadataDbHelper.markEntryAsAvailable(db, mWordList.mId, mWordList.mVersion); } else if (MetadataDbHelper.STATUS_AVAILABLE != status) { // Should never happen @@ -136,9 +130,6 @@ public final class ActionBatch { // Download it. DebugLogUtils.l("Upgrade word list, downloading", mWordList.mRemoteFilename); - // TODO: if DownloadManager is disabled or not installed, download by ourselves - if (null == manager) return; - // This is an upgraded word list: we should download it. // Adding a disambiguator to circumvent a bug in older versions of DownloadManager. // DownloadManager also stupidly cuts the extension to replace with its own that it @@ -293,13 +284,8 @@ public final class ActionBatch { } // The word list is still downloading. Cancel the download and revert the // word list status to "available". - final DownloadManager manager = - (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); - if (null != manager) { - // If we can't cancel the download because DownloadManager is not available, - // we still need to mark the entry as available. - manager.remove(values.getAsLong(MetadataDbHelper.PENDINGID_COLUMN)); - } + final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); + manager.remove(values.getAsLong(MetadataDbHelper.PENDINGID_COLUMN)); MetadataDbHelper.markEntryAsAvailable(db, mWordList.mId, mWordList.mVersion); } } @@ -338,8 +324,9 @@ public final class ActionBatch { MetadataDbHelper.TYPE_BULK, MetadataDbHelper.STATUS_AVAILABLE, mWordList.mId, mWordList.mLocale, mWordList.mDescription, null == mWordList.mLocalFilename ? "" : mWordList.mLocalFilename, - mWordList.mRemoteFilename, mWordList.mLastUpdate, mWordList.mChecksum, - mWordList.mFileSize, mWordList.mVersion, mWordList.mFormatVersion); + mWordList.mRemoteFilename, mWordList.mLastUpdate, mWordList.mRawChecksum, + mWordList.mChecksum, mWordList.mFileSize, mWordList.mVersion, + mWordList.mFormatVersion); PrivateLog.log("Insert 'available' record for " + mWordList.mDescription + " and locale " + mWordList.mLocale); db.insert(MetadataDbHelper.METADATA_TABLE_NAME, null, values); @@ -387,7 +374,7 @@ public final class ActionBatch { final ContentValues values = MetadataDbHelper.makeContentValues(0, MetadataDbHelper.TYPE_BULK, MetadataDbHelper.STATUS_INSTALLED, mWordList.mId, mWordList.mLocale, mWordList.mDescription, - "", mWordList.mRemoteFilename, mWordList.mLastUpdate, + "", mWordList.mRemoteFilename, mWordList.mLastUpdate, mWordList.mRawChecksum, mWordList.mChecksum, mWordList.mFileSize, mWordList.mVersion, mWordList.mFormatVersion); PrivateLog.log("Insert 'preinstalled' record for " + mWordList.mDescription @@ -429,8 +416,9 @@ public final class ActionBatch { oldValues.getAsInteger(MetadataDbHelper.STATUS_COLUMN), mWordList.mId, mWordList.mLocale, mWordList.mDescription, oldValues.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN), - mWordList.mRemoteFilename, mWordList.mLastUpdate, mWordList.mChecksum, - mWordList.mFileSize, mWordList.mVersion, mWordList.mFormatVersion); + mWordList.mRemoteFilename, mWordList.mLastUpdate, mWordList.mRawChecksum, + mWordList.mChecksum, mWordList.mFileSize, mWordList.mVersion, + mWordList.mFormatVersion); PrivateLog.log("Updating record for " + mWordList.mDescription + " and locale " + mWordList.mLocale); db.update(MetadataDbHelper.METADATA_TABLE_NAME, values, @@ -611,7 +599,7 @@ public final class ActionBatch { private final Queue<Action> mActions; public ActionBatch() { - mActions = new LinkedList<Action>(); + mActions = new LinkedList<>(); } public void add(final Action a) { diff --git a/java/src/com/android/inputmethod/dictionarypack/CommonPreferences.java b/java/src/com/android/inputmethod/dictionarypack/CommonPreferences.java index 7c27e6d51..3d0e29ed0 100644 --- a/java/src/com/android/inputmethod/dictionarypack/CommonPreferences.java +++ b/java/src/com/android/inputmethod/dictionarypack/CommonPreferences.java @@ -23,7 +23,7 @@ public final class CommonPreferences { private static final String COMMON_PREFERENCES_NAME = "LatinImeDictPrefs"; public static SharedPreferences getCommonPreferences(final Context context) { - return context.getSharedPreferences(COMMON_PREFERENCES_NAME, Context.MODE_WORLD_READABLE); + return context.getSharedPreferences(COMMON_PREFERENCES_NAME, 0); } public static void enable(final SharedPreferences pref, final String id) { diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java index 88b5032e3..1d84e5888 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java +++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java @@ -29,7 +29,6 @@ import android.view.View; import android.widget.ProgressBar; public class DictionaryDownloadProgressBar extends ProgressBar { - @SuppressWarnings("unused") private static final String TAG = DictionaryDownloadProgressBar.class.getSimpleName(); private static final int NOT_A_DOWNLOADMANAGER_PENDING_ID = 0; @@ -100,32 +99,28 @@ public class DictionaryDownloadProgressBar extends ProgressBar { @Override protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); mIsCurrentlyAttachedToWindow = false; updateReporterThreadRunningStatusAccordingToVisibility(); } private class UpdaterThread extends Thread { private final static int REPORT_PERIOD = 150; // how often to report progress, in ms - final DownloadManager mDownloadManager; + final DownloadManagerWrapper mDownloadManagerWrapper; final int mId; public UpdaterThread(final Context context, final int id) { super(); - mDownloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + mDownloadManagerWrapper = new DownloadManagerWrapper(context); mId = id; } @Override public void run() { try { - // It's almost impossible that mDownloadManager is null (it would mean it has been - // disabled between pressing the 'install' button and displaying the progress - // bar), but just in case. - if (null == mDownloadManager) return; final UpdateHelper updateHelper = new UpdateHelper(); final Query query = new Query().setFilterById(mId); - int lastProgress = 0; setIndeterminate(true); while (!isInterrupted()) { - final Cursor cursor = mDownloadManager.query(query); + final Cursor cursor = mDownloadManagerWrapper.query(query); if (null == cursor) { // Can't contact DownloadManager: this should never happen. return; diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java index 13c07de35..8e026171d 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java +++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java @@ -18,8 +18,6 @@ package com.android.inputmethod.dictionarypack; import android.view.View; -import com.android.inputmethod.latin.utils.CollectionUtils; - import java.util.ArrayList; import java.util.HashMap; @@ -39,8 +37,8 @@ public class DictionaryListInterfaceState { public int mStatus = MetadataDbHelper.STATUS_UNKNOWN; } - private HashMap<String, State> mWordlistToState = CollectionUtils.newHashMap(); - private ArrayList<View> mViewCache = CollectionUtils.newArrayList(); + private HashMap<String, State> mWordlistToState = new HashMap<>(); + private ArrayList<View> mViewCache = new ArrayList<>(); public boolean isOpen(final String wordlistId) { final State state = mWordlistToState.get(wordlistId); diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java index 1d9b9991e..f5bd84c8c 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java +++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java @@ -89,10 +89,13 @@ public final class DictionaryProvider extends ContentProvider { private static final class WordListInfo { public final String mId; public final String mLocale; + public final String mRawChecksum; public final int mMatchLevel; - public WordListInfo(final String id, final String locale, final int matchLevel) { + public WordListInfo(final String id, final String locale, final String rawChecksum, + final int matchLevel) { mId = id; mLocale = locale; + mRawChecksum = rawChecksum; mMatchLevel = matchLevel; } } @@ -106,7 +109,8 @@ public final class DictionaryProvider extends ContentProvider { private static final class ResourcePathCursor extends AbstractCursor { // Column names for the cursor returned by this content provider. - static private final String[] columnNames = { "id", "locale" }; + static private final String[] columnNames = { MetadataDbHelper.WORDLISTID_COLUMN, + MetadataDbHelper.LOCALE_COLUMN, MetadataDbHelper.RAW_CHECKSUM_COLUMN }; // The list of word lists served by this provider that match the client request. final WordListInfo[] mWordLists; @@ -141,6 +145,7 @@ public final class DictionaryProvider extends ContentProvider { switch (column) { case 0: return mWordLists[mPos].mId; case 1: return mWordLists[mPos].mLocale; + case 2: return mWordLists[mPos].mRawChecksum; default : return null; } } @@ -350,12 +355,15 @@ public final class DictionaryProvider extends ContentProvider { clientId); if (null == results) { return Collections.<WordListInfo>emptyList(); - } else { - final HashMap<String, WordListInfo> dicts = new HashMap<String, WordListInfo>(); + } + try { + final HashMap<String, WordListInfo> dicts = new HashMap<>(); final int idIndex = results.getColumnIndex(MetadataDbHelper.WORDLISTID_COLUMN); final int localeIndex = results.getColumnIndex(MetadataDbHelper.LOCALE_COLUMN); final int localFileNameIndex = results.getColumnIndex(MetadataDbHelper.LOCAL_FILENAME_COLUMN); + final int rawChecksumIndex = + results.getColumnIndex(MetadataDbHelper.RAW_CHECKSUM_COLUMN); final int statusIndex = results.getColumnIndex(MetadataDbHelper.STATUS_COLUMN); if (results.moveToFirst()) { do { @@ -378,6 +386,7 @@ public final class DictionaryProvider extends ContentProvider { } final String wordListLocale = results.getString(localeIndex); final String wordListLocalFilename = results.getString(localFileNameIndex); + final String wordListRawChecksum = results.getString(rawChecksumIndex); final int wordListStatus = results.getInt(statusIndex); // Test the requested locale against this wordlist locale. The requested locale // has to either match exactly or be more specific than the dictionary - a @@ -411,13 +420,14 @@ public final class DictionaryProvider extends ContentProvider { final WordListInfo currentBestMatch = dicts.get(wordListCategory); if (null == currentBestMatch || currentBestMatch.mMatchLevel < matchLevel) { - dicts.put(wordListCategory, - new WordListInfo(wordListId, wordListLocale, matchLevel)); + dicts.put(wordListCategory, new WordListInfo(wordListId, wordListLocale, + wordListRawChecksum, matchLevel)); } } while (results.moveToNext()); } - results.close(); return Collections.unmodifiableCollection(dicts.values()); + } finally { + results.close(); } } diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java index 7bbd041e7..11982fa65 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java +++ b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java @@ -33,13 +33,13 @@ import android.preference.PreferenceGroup; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.Log; -import android.view.animation.AnimationUtils; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.view.animation.AnimationUtils; import com.android.inputmethod.latin.R; @@ -67,8 +67,8 @@ public final class DictionarySettingsFragment extends PreferenceFragment private boolean mChangedSettings; private DictionaryListInterfaceState mDictionaryListInterfaceState = new DictionaryListInterfaceState(); - private TreeMap<String, WordListPreference> mCurrentPreferenceMap = - new TreeMap<String, WordListPreference>(); // never null + // never null + private TreeMap<String, WordListPreference> mCurrentPreferenceMap = new TreeMap<>(); private final BroadcastReceiver mConnectivityChangedReceiver = new BroadcastReceiver() { @Override @@ -280,62 +280,72 @@ public final class DictionarySettingsFragment extends PreferenceFragment : activity.getContentResolver().query(contentUri, null, null, null, null); if (null == cursor) { - final ArrayList<Preference> result = new ArrayList<Preference>(); + final ArrayList<Preference> result = new ArrayList<>(); result.add(createErrorMessage(activity, R.string.cannot_connect_to_dict_service)); return result; - } else if (!cursor.moveToFirst()) { - final ArrayList<Preference> result = new ArrayList<Preference>(); - result.add(createErrorMessage(activity, R.string.no_dictionaries_available)); - cursor.close(); - return result; - } else { - final String systemLocaleString = Locale.getDefault().toString(); - final TreeMap<String, WordListPreference> prefMap = - new TreeMap<String, WordListPreference>(); - final int idIndex = cursor.getColumnIndex(MetadataDbHelper.WORDLISTID_COLUMN); - final int versionIndex = cursor.getColumnIndex(MetadataDbHelper.VERSION_COLUMN); - final int localeIndex = cursor.getColumnIndex(MetadataDbHelper.LOCALE_COLUMN); - final int descriptionIndex = cursor.getColumnIndex(MetadataDbHelper.DESCRIPTION_COLUMN); - final int statusIndex = cursor.getColumnIndex(MetadataDbHelper.STATUS_COLUMN); - final int filesizeIndex = cursor.getColumnIndex(MetadataDbHelper.FILESIZE_COLUMN); - do { - final String wordlistId = cursor.getString(idIndex); - final int version = cursor.getInt(versionIndex); - final String localeString = cursor.getString(localeIndex); - final Locale locale = new Locale(localeString); - final String description = cursor.getString(descriptionIndex); - final int status = cursor.getInt(statusIndex); - final int matchLevel = LocaleUtils.getMatchLevel(systemLocaleString, localeString); - final String matchLevelString = LocaleUtils.getMatchLevelSortedString(matchLevel); - final int filesize = cursor.getInt(filesizeIndex); - // The key is sorted in lexicographic order, according to the match level, then - // the description. - final String key = matchLevelString + "." + description + "." + wordlistId; - final WordListPreference existingPref = prefMap.get(key); - if (null == existingPref || existingPref.hasPriorityOver(status)) { - final WordListPreference oldPreference = mCurrentPreferenceMap.get(key); - final WordListPreference pref; - if (null != oldPreference - && oldPreference.mVersion == version - && oldPreference.mLocale.equals(locale)) { - // If the old preference has all the new attributes, reuse it. We test - // for version and locale because although attributes other than status - // need to be the same, others have been tested through the key of the - // map. Also, status may differ so we don't want to use #equals() here. - pref = oldPreference; - pref.setStatus(status); - } else { - // Otherwise, discard it and create a new one instead. - pref = new WordListPreference(activity, mDictionaryListInterfaceState, - mClientId, wordlistId, version, locale, description, status, - filesize); + } + try { + if (!cursor.moveToFirst()) { + final ArrayList<Preference> result = new ArrayList<>(); + result.add(createErrorMessage(activity, R.string.no_dictionaries_available)); + return result; + } else { + final String systemLocaleString = Locale.getDefault().toString(); + final TreeMap<String, WordListPreference> prefMap = new TreeMap<>(); + final int idIndex = cursor.getColumnIndex(MetadataDbHelper.WORDLISTID_COLUMN); + final int versionIndex = cursor.getColumnIndex(MetadataDbHelper.VERSION_COLUMN); + final int localeIndex = cursor.getColumnIndex(MetadataDbHelper.LOCALE_COLUMN); + final int descriptionIndex = + cursor.getColumnIndex(MetadataDbHelper.DESCRIPTION_COLUMN); + final int statusIndex = cursor.getColumnIndex(MetadataDbHelper.STATUS_COLUMN); + final int filesizeIndex = cursor.getColumnIndex(MetadataDbHelper.FILESIZE_COLUMN); + do { + final String wordlistId = cursor.getString(idIndex); + final int version = cursor.getInt(versionIndex); + final String localeString = cursor.getString(localeIndex); + final Locale locale = new Locale(localeString); + final String description = cursor.getString(descriptionIndex); + final int status = cursor.getInt(statusIndex); + final int matchLevel = + LocaleUtils.getMatchLevel(systemLocaleString, localeString); + final String matchLevelString = + LocaleUtils.getMatchLevelSortedString(matchLevel); + final int filesize = cursor.getInt(filesizeIndex); + // The key is sorted in lexicographic order, according to the match level, then + // the description. + final String key = matchLevelString + "." + description + "." + wordlistId; + final WordListPreference existingPref = prefMap.get(key); + if (null == existingPref || existingPref.hasPriorityOver(status)) { + final WordListPreference oldPreference = mCurrentPreferenceMap.get(key); + final WordListPreference pref; + if (null != oldPreference + && oldPreference.mVersion == version + && oldPreference.hasStatus(status) + && oldPreference.mLocale.equals(locale)) { + // If the old preference has all the new attributes, reuse it. Ideally, + // we should reuse the old pref even if its status is different and call + // setStatus here, but setStatus calls Preference#setSummary() which + // needs to be done on the UI thread and we're not on the UI thread + // here. We could do all this work on the UI thread, but in this case + // it's probably lighter to stay on a background thread and throw this + // old preference out. + pref = oldPreference; + } else { + // Otherwise, discard it and create a new one instead. + // TODO: when the status is different from the old one, we need to + // animate the old one out before animating the new one in. + pref = new WordListPreference(activity, mDictionaryListInterfaceState, + mClientId, wordlistId, version, locale, description, status, + filesize); + } + prefMap.put(key, pref); } - prefMap.put(key, pref); - } - } while (cursor.moveToNext()); + } while (cursor.moveToNext()); + mCurrentPreferenceMap = prefMap; + return prefMap.values(); + } + } finally { cursor.close(); - mCurrentPreferenceMap = prefMap; - return prefMap.values(); } } diff --git a/java/src/com/android/inputmethod/dictionarypack/DownloadManagerWrapper.java b/java/src/com/android/inputmethod/dictionarypack/DownloadManagerWrapper.java new file mode 100644 index 000000000..75cc7d4cb --- /dev/null +++ b/java/src/com/android/inputmethod/dictionarypack/DownloadManagerWrapper.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.inputmethod.dictionarypack; + +import android.app.DownloadManager; +import android.app.DownloadManager.Query; +import android.app.DownloadManager.Request; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteException; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import java.io.FileNotFoundException; + +/** + * A class to help with calling DownloadManager methods. + * + * Mostly, the problem here is that most methods from DownloadManager may throw SQL exceptions if + * they can't open the database on disk. We want to avoid crashing in these cases but can't do + * much more, so this class insulates the callers from these. SQLiteException also inherit from + * RuntimeException so they are unchecked :( + * While we're at it, we also insulate callers from the cases where DownloadManager is disabled, + * and getSystemService returns null. + */ +public class DownloadManagerWrapper { + private final static String TAG = DownloadManagerWrapper.class.getSimpleName(); + private final DownloadManager mDownloadManager; + + public DownloadManagerWrapper(final Context context) { + this((DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE)); + } + + private DownloadManagerWrapper(final DownloadManager downloadManager) { + mDownloadManager = downloadManager; + } + + public void remove(final long... ids) { + try { + if (null != mDownloadManager) { + mDownloadManager.remove(ids); + } + } catch (SQLiteException e) { + // We couldn't remove the file from DownloadManager. Apparently, the database can't + // be opened. It may be a problem with file system corruption. In any case, there is + // not much we can do apart from avoiding crashing. + Log.e(TAG, "Can't remove files with ID " + ids + " from download manager", e); + } catch (IllegalArgumentException e) { + // Not sure how this can happen, but it could be another case where the provider + // is disabled. Or it could be a bug in older versions of the framework. + Log.e(TAG, "Can't find the content URL for DownloadManager?", e); + } + } + + public ParcelFileDescriptor openDownloadedFile(final long fileId) throws FileNotFoundException { + try { + if (null != mDownloadManager) { + return mDownloadManager.openDownloadedFile(fileId); + } + } catch (SQLiteException e) { + Log.e(TAG, "Can't open downloaded file with ID " + fileId, e); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Can't find the content URL for DownloadManager?", e); + } + // We come here if mDownloadManager is null or if an exception was thrown. + throw new FileNotFoundException(); + } + + public Cursor query(final Query query) { + try { + if (null != mDownloadManager) { + return mDownloadManager.query(query); + } + } catch (SQLiteException e) { + Log.e(TAG, "Can't query the download manager", e); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Can't find the content URL for DownloadManager?", e); + } + // We come here if mDownloadManager is null or if an exception was thrown. + return null; + } + + public long enqueue(final Request request) { + try { + if (null != mDownloadManager) { + return mDownloadManager.enqueue(request); + } + } catch (SQLiteException e) { + Log.e(TAG, "Can't enqueue a request with the download manager", e); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Can't find the content URL for DownloadManager?", e); + } + return 0; + } +} diff --git a/java/src/com/android/inputmethod/dictionarypack/LocaleUtils.java b/java/src/com/android/inputmethod/dictionarypack/LocaleUtils.java index 77f67b8a3..4f0805c5c 100644 --- a/java/src/com/android/inputmethod/dictionarypack/LocaleUtils.java +++ b/java/src/com/android/inputmethod/dictionarypack/LocaleUtils.java @@ -175,7 +175,7 @@ public final class LocaleUtils { return saveLocale; } - private static final HashMap<String, Locale> sLocaleCache = new HashMap<String, Locale>(); + private static final HashMap<String, Locale> sLocaleCache = new HashMap<>(); /** * Creates a locale from a string specification. diff --git a/java/src/com/android/inputmethod/dictionarypack/MD5Calculator.java b/java/src/com/android/inputmethod/dictionarypack/MD5Calculator.java index e47e86e4b..ccd054c84 100644 --- a/java/src/com/android/inputmethod/dictionarypack/MD5Calculator.java +++ b/java/src/com/android/inputmethod/dictionarypack/MD5Calculator.java @@ -20,7 +20,7 @@ import java.io.InputStream; import java.io.IOException; import java.security.MessageDigest; -final class MD5Calculator { +public final class MD5Calculator { private MD5Calculator() {} // This helper class is not instantiable public static String checksum(final InputStream in) throws IOException { diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java index ff5aba6d8..17dd781d5 100644 --- a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java +++ b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java @@ -20,6 +20,7 @@ import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.text.TextUtils; import android.util.Log; @@ -45,10 +46,8 @@ public class MetadataDbHelper extends SQLiteOpenHelper { // This is the first released version of the database that implements CLIENTID. It is // used to identify the versions for upgrades. This should never change going forward. private static final int METADATA_DATABASE_VERSION_WITH_CLIENTID = 6; - // This is the current database version. It should be updated when the database schema - // gets updated. It is passed to the framework constructor of SQLiteOpenHelper, so - // that's what the framework uses to track our database version. - private static final int METADATA_DATABASE_VERSION = 6; + // The current database version. + private static final int CURRENT_METADATA_DATABASE_VERSION = 9; private final static long NOT_A_DOWNLOAD_ID = -1; @@ -68,7 +67,8 @@ public class MetadataDbHelper extends SQLiteOpenHelper { public static final String VERSION_COLUMN = "version"; public static final String FORMATVERSION_COLUMN = "formatversion"; public static final String FLAGS_COLUMN = "flags"; - public static final int COLUMN_COUNT = 13; + public static final String RAW_CHECKSUM_COLUMN = "rawChecksum"; + public static final int COLUMN_COUNT = 14; private static final String CLIENT_CLIENT_ID_COLUMN = "clientid"; private static final String CLIENT_METADATA_URI_COLUMN = "uri"; @@ -121,8 +121,9 @@ public class MetadataDbHelper extends SQLiteOpenHelper { + CHECKSUM_COLUMN + " TEXT, " + FILESIZE_COLUMN + " INTEGER, " + VERSION_COLUMN + " INTEGER," - + FORMATVERSION_COLUMN + " INTEGER," - + FLAGS_COLUMN + " INTEGER," + + FORMATVERSION_COLUMN + " INTEGER, " + + FLAGS_COLUMN + " INTEGER, " + + RAW_CHECKSUM_COLUMN + " TEXT," + "PRIMARY KEY (" + WORDLISTID_COLUMN + "," + VERSION_COLUMN + "));"; private static final String METADATA_CREATE_CLIENT_TABLE = "CREATE TABLE IF NOT EXISTS " + CLIENT_TABLE_NAME + " (" @@ -138,7 +139,8 @@ public class MetadataDbHelper extends SQLiteOpenHelper { static final String[] METADATA_TABLE_COLUMNS = { PENDINGID_COLUMN, TYPE_COLUMN, STATUS_COLUMN, WORDLISTID_COLUMN, LOCALE_COLUMN, DESCRIPTION_COLUMN, LOCAL_FILENAME_COLUMN, REMOTE_FILENAME_COLUMN, DATE_COLUMN, CHECKSUM_COLUMN, - FILESIZE_COLUMN, VERSION_COLUMN, FORMATVERSION_COLUMN, FLAGS_COLUMN }; + FILESIZE_COLUMN, VERSION_COLUMN, FORMATVERSION_COLUMN, FLAGS_COLUMN, + RAW_CHECKSUM_COLUMN }; // List of all client table columns. static final String[] CLIENT_TABLE_COLUMNS = { CLIENT_CLIENT_ID_COLUMN, CLIENT_METADATA_URI_COLUMN, CLIENT_PENDINGID_COLUMN, FLAGS_COLUMN }; @@ -158,7 +160,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { // this legacy database. New clients should make sure to always pass a client ID so as // to avoid conflicts. final String clientId = null != clientIdOrNull ? clientIdOrNull : ""; - if (null == sInstanceMap) sInstanceMap = new TreeMap<String, MetadataDbHelper>(); + if (null == sInstanceMap) sInstanceMap = new TreeMap<>(); MetadataDbHelper helper = sInstanceMap.get(clientId); if (null == helper) { helper = new MetadataDbHelper(context, clientId); @@ -169,7 +171,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { private MetadataDbHelper(final Context context, final String clientId) { super(context, METADATA_DATABASE_NAME_STEM + (TextUtils.isEmpty(clientId) ? "" : "." + clientId), - null, METADATA_DATABASE_VERSION); + null, CURRENT_METADATA_DATABASE_VERSION); mContext = context; mClientId = clientId; } @@ -217,28 +219,68 @@ public class MetadataDbHelper extends SQLiteOpenHelper { createClientTable(db); } + private void addRawChecksumColumnUnlessPresent(final SQLiteDatabase db, final String clientId) { + try { + db.execSQL("SELECT " + RAW_CHECKSUM_COLUMN + " FROM " + + METADATA_TABLE_NAME + " LIMIT 0;"); + } catch (SQLiteException e) { + Log.i(TAG, "No " + RAW_CHECKSUM_COLUMN + " column : creating it"); + db.execSQL("ALTER TABLE " + METADATA_TABLE_NAME + " ADD COLUMN " + + RAW_CHECKSUM_COLUMN + " TEXT;"); + } + } + /** * Upgrade the database. Upgrade from version 3 is supported. + * Version 3 has a DB named METADATA_DATABASE_NAME_STEM containing a table METADATA_TABLE_NAME. + * Version 6 and above has a DB named METADATA_DATABASE_NAME_STEM containing a + * table CLIENT_TABLE_NAME, and for each client a table called METADATA_TABLE_STEM + "." + the + * name of the client and contains a table METADATA_TABLE_NAME. + * For schemas, see the above create statements. The schemas have never changed so far. + * + * This method is called by the framework. See {@link SQLiteOpenHelper#onUpgrade} + * @param db The database we are upgrading + * @param oldVersion The old database version (the one on the disk) + * @param newVersion The new database version as supplied to the constructor of SQLiteOpenHelper */ @Override public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) { if (METADATA_DATABASE_INITIAL_VERSION == oldVersion - && METADATA_DATABASE_VERSION_WITH_CLIENTID == newVersion) { + && METADATA_DATABASE_VERSION_WITH_CLIENTID <= newVersion + && CURRENT_METADATA_DATABASE_VERSION >= newVersion) { // Upgrade from version METADATA_DATABASE_INITIAL_VERSION to version // METADATA_DATABASE_VERSION_WITH_CLIENT_ID + // Only the default database should contain the client table, so we test for mClientId. if (TextUtils.isEmpty(mClientId)) { - // Only the default database should contain the client table. - // Anyway in version 3 only the default table existed so the emptyness + // Anyway in version 3 only the default table existed so the emptiness // test should always be true, but better check to be sure. createClientTable(db); } + } else if (METADATA_DATABASE_VERSION_WITH_CLIENTID < newVersion + && CURRENT_METADATA_DATABASE_VERSION >= newVersion) { + // Here we drop the client table, so that all clients send us their information again. + // The client table contains the URL to hit to update the available dictionaries list, + // but the info about the dictionaries themselves is stored in the table called + // METADATA_TABLE_NAME and we want to keep it, so we only drop the client table. + db.execSQL("DROP TABLE IF EXISTS " + CLIENT_TABLE_NAME); + // Only the default database should contain the client table, so we test for mClientId. + if (TextUtils.isEmpty(mClientId)) { + createClientTable(db); + } } else { - // Version 3 was the earliest version, so we should never come here. If we do, we - // have no idea what this database is, so we'd better wipe it off. + // If we're not in the above case, either we are upgrading from an earlier versionCode + // and we should wipe the database, or we are handling a version we never heard about + // (can only be a bug) so it's safer to wipe the database. db.execSQL("DROP TABLE IF EXISTS " + METADATA_TABLE_NAME); db.execSQL("DROP TABLE IF EXISTS " + CLIENT_TABLE_NAME); onCreate(db); } + // A rawChecksum column that did not exist in the previous versions was added that + // corresponds to the md5 checksum of the file after decompression/decryption. This is to + // strengthen the system against corrupted dictionary files. + // The most secure way to upgrade a database is to just test for the column presence, and + // add it if it's not there. + addRawChecksumColumnUnlessPresent(db, mClientId); } /** @@ -410,7 +452,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { public static ContentValues makeContentValues(final int pendingId, final int type, final int status, final String wordlistId, final String locale, final String description, final String filename, final String url, final long date, - final String checksum, final long filesize, final int version, + final String rawChecksum, final String checksum, final long filesize, final int version, final int formatVersion) { final ContentValues result = new ContentValues(COLUMN_COUNT); result.put(PENDINGID_COLUMN, pendingId); @@ -422,6 +464,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { result.put(LOCAL_FILENAME_COLUMN, filename); result.put(REMOTE_FILENAME_COLUMN, url); result.put(DATE_COLUMN, date); + result.put(RAW_CHECKSUM_COLUMN, rawChecksum); result.put(CHECKSUM_COLUMN, checksum); result.put(FILESIZE_COLUMN, filesize); result.put(VERSION_COLUMN, version); @@ -457,6 +500,8 @@ public class MetadataDbHelper extends SQLiteOpenHelper { if (null == result.get(REMOTE_FILENAME_COLUMN)) result.put(REMOTE_FILENAME_COLUMN, ""); // 0 for the update date : 1970/1/1. Unless specified. if (null == result.get(DATE_COLUMN)) result.put(DATE_COLUMN, 0); + // Raw checksum unknown unless specified + if (null == result.get(RAW_CHECKSUM_COLUMN)) result.put(RAW_CHECKSUM_COLUMN, ""); // Checksum unknown unless specified if (null == result.get(CHECKSUM_COLUMN)) result.put(CHECKSUM_COLUMN, ""); // No filesize unless specified @@ -504,6 +549,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { putStringResult(result, cursor, LOCAL_FILENAME_COLUMN); putStringResult(result, cursor, REMOTE_FILENAME_COLUMN); putIntResult(result, cursor, DATE_COLUMN); + putStringResult(result, cursor, RAW_CHECKSUM_COLUMN); putStringResult(result, cursor, CHECKSUM_COLUMN); putIntResult(result, cursor, FILESIZE_COLUMN); putIntResult(result, cursor, VERSION_COLUMN); @@ -533,12 +579,17 @@ public class MetadataDbHelper extends SQLiteOpenHelper { PENDINGID_COLUMN + "= ?", new String[] { Long.toString(id) }, null, null, null); - // There should never be more than one result. If because of some bug there are, returning - // only one result is the right thing to do, because we couldn't handle several anyway - // and we should still handle one. - final ContentValues result = getFirstLineAsContentValues(cursor); - cursor.close(); - return result; + if (null == cursor) { + return null; + } + try { + // There should never be more than one result. If because of some bug there are, + // returning only one result is the right thing to do, because we couldn't handle + // several anyway and we should still handle one. + return getFirstLineAsContentValues(cursor); + } finally { + cursor.close(); + } } /** @@ -559,11 +610,16 @@ public class MetadataDbHelper extends SQLiteOpenHelper { new String[] { id, Integer.toString(STATUS_INSTALLED), Integer.toString(STATUS_DELETING) }, null, null, null); - // There should only be one result, but if there are several, we can't tell which - // is the best, so we just return the first one. - final ContentValues result = getFirstLineAsContentValues(cursor); - cursor.close(); - return result; + if (null == cursor) { + return null; + } + try { + // There should only be one result, but if there are several, we can't tell which + // is the best, so we just return the first one. + return getFirstLineAsContentValues(cursor); + } finally { + cursor.close(); + } } /** @@ -583,7 +639,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { public static ArrayList<DownloadRecord> getDownloadRecordsForDownloadId(final Context context, final long downloadId) { final SQLiteDatabase defaultDb = getDb(context, ""); - final ArrayList<DownloadRecord> results = new ArrayList<DownloadRecord>(); + final ArrayList<DownloadRecord> results = new ArrayList<>(); final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME, CLIENT_TABLE_COLUMNS, null, null, null, null, null); try { @@ -622,10 +678,15 @@ public class MetadataDbHelper extends SQLiteOpenHelper { METADATA_TABLE_COLUMNS, WORDLISTID_COLUMN + "= ? AND " + VERSION_COLUMN + "= ?", new String[] { id, Integer.toString(version) }, null, null, null); - // This is a lookup by primary key, so there can't be more than one result. - final ContentValues result = getFirstLineAsContentValues(cursor); - cursor.close(); - return result; + if (null == cursor) { + return null; + } + try { + // This is a lookup by primary key, so there can't be more than one result. + return getFirstLineAsContentValues(cursor); + } finally { + cursor.close(); + } } /** @@ -641,10 +702,15 @@ public class MetadataDbHelper extends SQLiteOpenHelper { METADATA_TABLE_COLUMNS, WORDLISTID_COLUMN + "= ?", new String[] { id }, null, null, VERSION_COLUMN + " DESC", "1"); - // This is a lookup by primary key, so there can't be more than one result. - final ContentValues result = getFirstLineAsContentValues(cursor); - cursor.close(); - return result; + if (null == cursor) { + return null; + } + try { + // This is a lookup by primary key, so there can't be more than one result. + return getFirstLineAsContentValues(cursor); + } finally { + cursor.close(); + } } /** @@ -857,7 +923,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { // - Remove the old entry from the table // - Erase the old file // We start by gathering the names of the files we should delete. - final List<String> filenames = new LinkedList<String>(); + final List<String> filenames = new LinkedList<>(); final Cursor c = db.query(METADATA_TABLE_NAME, new String[] { LOCAL_FILENAME_COLUMN }, LOCALE_COLUMN + " = ? AND " + diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataHandler.java b/java/src/com/android/inputmethod/dictionarypack/MetadataHandler.java index a0147b6d6..d66b69050 100644 --- a/java/src/com/android/inputmethod/dictionarypack/MetadataHandler.java +++ b/java/src/com/android/inputmethod/dictionarypack/MetadataHandler.java @@ -43,9 +43,8 @@ public class MetadataHandler { * @return the constructed list of wordlist metadata. */ private static List<WordListMetadata> makeMetadataObject(final Cursor results) { - final ArrayList<WordListMetadata> buildingMetadata = new ArrayList<WordListMetadata>(); - - if (results.moveToFirst()) { + final ArrayList<WordListMetadata> buildingMetadata = new ArrayList<>(); + if (null != results && results.moveToFirst()) { final int localeColumn = results.getColumnIndex(MetadataDbHelper.LOCALE_COLUMN); final int typeColumn = results.getColumnIndex(MetadataDbHelper.TYPE_COLUMN); final int descriptionColumn = @@ -53,6 +52,8 @@ public class MetadataHandler { final int idIndex = results.getColumnIndex(MetadataDbHelper.WORDLISTID_COLUMN); final int updateIndex = results.getColumnIndex(MetadataDbHelper.DATE_COLUMN); final int fileSizeIndex = results.getColumnIndex(MetadataDbHelper.FILESIZE_COLUMN); + final int rawChecksumIndex = + results.getColumnIndex(MetadataDbHelper.RAW_CHECKSUM_COLUMN); final int checksumIndex = results.getColumnIndex(MetadataDbHelper.CHECKSUM_COLUMN); final int localFilenameIndex = results.getColumnIndex(MetadataDbHelper.LOCAL_FILENAME_COLUMN); @@ -61,13 +62,13 @@ public class MetadataHandler { final int versionIndex = results.getColumnIndex(MetadataDbHelper.VERSION_COLUMN); final int formatVersionIndex = results.getColumnIndex(MetadataDbHelper.FORMATVERSION_COLUMN); - do { buildingMetadata.add(new WordListMetadata(results.getString(idIndex), results.getInt(typeColumn), results.getString(descriptionColumn), results.getLong(updateIndex), results.getLong(fileSizeIndex), + results.getString(rawChecksumIndex), results.getString(checksumIndex), results.getString(localFilenameIndex), results.getString(remoteFilenameIndex), @@ -75,8 +76,6 @@ public class MetadataHandler { results.getInt(formatVersionIndex), 0, results.getString(localeColumn))); } while (results.moveToNext()); - - results.close(); } return Collections.unmodifiableList(buildingMetadata); } @@ -92,9 +91,14 @@ public class MetadataHandler { // If clientId is null, we get a cursor on the default database (see // MetadataDbHelper#getInstance() for more on this) final Cursor results = MetadataDbHelper.queryCurrentMetadata(context, clientId); - final List<WordListMetadata> resultList = makeMetadataObject(results); - results.close(); - return resultList; + // If null, we should return makeMetadataObject(null), so we go through. + try { + return makeMetadataObject(results); + } finally { + if (null != results) { + results.close(); + } + } } /** diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataParser.java b/java/src/com/android/inputmethod/dictionarypack/MetadataParser.java index 27670fddf..52290cadc 100644 --- a/java/src/com/android/inputmethod/dictionarypack/MetadataParser.java +++ b/java/src/com/android/inputmethod/dictionarypack/MetadataParser.java @@ -37,6 +37,7 @@ public class MetadataParser { private static final String DESCRIPTION_FIELD_NAME = MetadataDbHelper.DESCRIPTION_COLUMN; private static final String UPDATE_FIELD_NAME = "update"; private static final String FILESIZE_FIELD_NAME = MetadataDbHelper.FILESIZE_COLUMN; + private static final String RAW_CHECKSUM_FIELD_NAME = MetadataDbHelper.RAW_CHECKSUM_COLUMN; private static final String CHECKSUM_FIELD_NAME = MetadataDbHelper.CHECKSUM_COLUMN; private static final String REMOTE_FILENAME_FIELD_NAME = MetadataDbHelper.REMOTE_FILENAME_COLUMN; @@ -51,7 +52,7 @@ public class MetadataParser { */ private static WordListMetadata parseOneWordList(final JsonReader reader) throws IOException, BadFormatException { - final TreeMap<String, String> arguments = new TreeMap<String, String>(); + final TreeMap<String, String> arguments = new TreeMap<>(); reader.beginObject(); while (reader.hasNext()) { final String name = reader.nextName(); @@ -80,6 +81,7 @@ public class MetadataParser { arguments.get(DESCRIPTION_FIELD_NAME), Long.parseLong(arguments.get(UPDATE_FIELD_NAME)), Long.parseLong(arguments.get(FILESIZE_FIELD_NAME)), + arguments.get(RAW_CHECKSUM_FIELD_NAME), arguments.get(CHECKSUM_FIELD_NAME), null, arguments.get(REMOTE_FILENAME_FIELD_NAME), @@ -98,7 +100,7 @@ public class MetadataParser { public static List<WordListMetadata> parseMetadata(final InputStreamReader input) throws IOException, BadFormatException { JsonReader reader = new JsonReader(input); - final ArrayList<WordListMetadata> readInfo = new ArrayList<WordListMetadata>(); + final ArrayList<WordListMetadata> readInfo = new ArrayList<>(); reader.beginArray(); while (reader.hasNext()) { final WordListMetadata thisMetadata = parseOneWordList(reader); diff --git a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java index 0e7c3bb7e..95a094232 100644 --- a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java +++ b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java @@ -177,7 +177,7 @@ public final class UpdateHandler { */ public static boolean tryUpdate(final Context context, final boolean updateNow) { // TODO: loop through all clients instead of only doing the default one. - final TreeSet<String> uris = new TreeSet<String>(); + final TreeSet<String> uris = new TreeSet<>(); final Cursor cursor = MetadataDbHelper.queryClientIds(context); if (null == cursor) return false; try { @@ -249,13 +249,7 @@ public final class UpdateHandler { metadataRequest.setVisibleInDownloadsUi( res.getBoolean(R.bool.metadata_downloads_visible_in_download_UI)); - final DownloadManager manager = - (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); - if (null == manager) { - // Download manager is not installed or disabled. - // TODO: fall back to self-managed download? - return; - } + final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); cancelUpdateWithDownloadManager(context, metadataUri, manager); final long downloadId; synchronized (sSharedIdProtector) { @@ -278,10 +272,10 @@ public final class UpdateHandler { * * @param context the context to open the database on * @param metadataUri the URI to cancel - * @param manager an instance of DownloadManager + * @param manager an wrapped instance of DownloadManager */ private static void cancelUpdateWithDownloadManager(final Context context, - final String metadataUri, final DownloadManager manager) { + final String metadataUri, final DownloadManagerWrapper manager) { synchronized (sSharedIdProtector) { final long metadataDownloadId = MetadataDbHelper.getMetadataDownloadIdForURI(context, metadataUri); @@ -306,10 +300,9 @@ public final class UpdateHandler { * @param clientId the ID of the client we want to cancel the update of */ public static void cancelUpdate(final Context context, final String clientId) { - final DownloadManager manager = - (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); final String metadataUri = MetadataDbHelper.getMetadataUriAsString(context, clientId); - if (null != manager) cancelUpdateWithDownloadManager(context, metadataUri, manager); + cancelUpdateWithDownloadManager(context, metadataUri, manager); } /** @@ -323,15 +316,15 @@ public final class UpdateHandler { * download request id, which is not known before submitting the request to the download * manager. Hence, it only updates the relevant line. * - * @param manager the download manager service to register the request with. + * @param manager a wrapped download manager service to register the request with. * @param request the request to register. * @param db the metadata database. * @param id the id of the word list. * @param version the version of the word list. * @return the download id returned by the download manager. */ - public static long registerDownloadRequest(final DownloadManager manager, final Request request, - final SQLiteDatabase db, final String id, final int version) { + public static long registerDownloadRequest(final DownloadManagerWrapper manager, + final Request request, final SQLiteDatabase db, final String id, final int version) { DebugLogUtils.l("RegisterDownloadRequest for word list id : ", id, ", version ", version); final long downloadId; synchronized (sSharedIdProtector) { @@ -345,8 +338,8 @@ public final class UpdateHandler { /** * Retrieve information about a specific download from DownloadManager. */ - private static CompletedDownloadInfo getCompletedDownloadInfo(final DownloadManager manager, - final long downloadId) { + private static CompletedDownloadInfo getCompletedDownloadInfo( + final DownloadManagerWrapper manager, final long downloadId) { final Query query = new Query().setFilterById(downloadId); final Cursor cursor = manager.query(query); @@ -425,8 +418,7 @@ public final class UpdateHandler { DebugLogUtils.l("DownloadFinished with id", fileId); if (NOT_AN_ID == fileId) return; // Spurious wake-up: ignore - final DownloadManager manager = - (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); final CompletedDownloadInfo downloadInfo = getCompletedDownloadInfo(manager, fileId); final ArrayList<DownloadRecord> recordList = @@ -517,7 +509,7 @@ public final class UpdateHandler { } private static boolean handleDownloadedFile(final Context context, - final DownloadRecord downloadRecord, final DownloadManager manager, + final DownloadRecord downloadRecord, final DownloadManagerWrapper manager, final long fileId) { try { // {@link handleWordList(Context,InputStream,ContentValues)}. @@ -565,7 +557,7 @@ public final class UpdateHandler { // Instantiation of a parameterized type is not possible in Java, so it's not possible to // return the same type of list that was passed - probably the same reason why Collections // does not do it. So we need to decide statically which concrete type to return. - return new LinkedList<T>(src); + return new LinkedList<>(src); } /** @@ -748,10 +740,10 @@ public final class UpdateHandler { final ActionBatch actions = new ActionBatch(); // Upgrade existing word lists DebugLogUtils.l("Comparing dictionaries"); - final Set<String> wordListIds = new TreeSet<String>(); + final Set<String> wordListIds = new TreeSet<>(); // TODO: Can these be null? - if (null == from) from = new ArrayList<WordListMetadata>(); - if (null == to) to = new ArrayList<WordListMetadata>(); + if (null == from) from = new ArrayList<>(); + if (null == to) to = new ArrayList<>(); for (WordListMetadata wlData : from) wordListIds.add(wlData.mId); for (WordListMetadata wlData : to) wordListIds.add(wlData.mId); for (String id : wordListIds) { diff --git a/java/src/com/android/inputmethod/dictionarypack/WordListMetadata.java b/java/src/com/android/inputmethod/dictionarypack/WordListMetadata.java index 69bff9597..9e510a68b 100644 --- a/java/src/com/android/inputmethod/dictionarypack/WordListMetadata.java +++ b/java/src/com/android/inputmethod/dictionarypack/WordListMetadata.java @@ -30,6 +30,7 @@ public class WordListMetadata { public final String mDescription; public final long mLastUpdate; public final long mFileSize; + public final String mRawChecksum; public final String mChecksum; public final String mLocalFilename; public final String mRemoteFilename; @@ -50,13 +51,15 @@ public class WordListMetadata { public WordListMetadata(final String id, final int type, final String description, final long lastUpdate, final long fileSize, - final String checksum, final String localFilename, final String remoteFilename, - final int version, final int formatVersion, final int flags, final String locale) { + final String rawChecksum, final String checksum, final String localFilename, + final String remoteFilename, final int version, final int formatVersion, + final int flags, final String locale) { mId = id; mType = type; mDescription = description; mLastUpdate = lastUpdate; // In milliseconds mFileSize = fileSize; + mRawChecksum = rawChecksum; mChecksum = checksum; mLocalFilename = localFilename; mRemoteFilename = remoteFilename; @@ -77,6 +80,7 @@ public class WordListMetadata { final String description = values.getAsString(MetadataDbHelper.DESCRIPTION_COLUMN); final Long lastUpdate = values.getAsLong(MetadataDbHelper.DATE_COLUMN); final Long fileSize = values.getAsLong(MetadataDbHelper.FILESIZE_COLUMN); + final String rawChecksum = values.getAsString(MetadataDbHelper.RAW_CHECKSUM_COLUMN); final String checksum = values.getAsString(MetadataDbHelper.CHECKSUM_COLUMN); final String localFilename = values.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN); final String remoteFilename = values.getAsString(MetadataDbHelper.REMOTE_FILENAME_COLUMN); @@ -98,8 +102,8 @@ public class WordListMetadata { || null == locale) { throw new IllegalArgumentException(); } - return new WordListMetadata(id, type, description, lastUpdate, fileSize, checksum, - localFilename, remoteFilename, version, formatVersion, flags, locale); + return new WordListMetadata(id, type, description, lastUpdate, fileSize, rawChecksum, + checksum, localFilename, remoteFilename, version, formatVersion, flags, locale); } @Override @@ -110,6 +114,7 @@ public class WordListMetadata { sb.append("\nDescription : ").append(mDescription); sb.append("\nLastUpdate : ").append(mLastUpdate); sb.append("\nFileSize : ").append(mFileSize); + sb.append("\nRawChecksum : ").append(mRawChecksum); sb.append("\nChecksum : ").append(mChecksum); sb.append("\nLocalFilename : ").append(mLocalFilename); sb.append("\nRemoteFilename : ").append(mRemoteFilename); diff --git a/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java b/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java index ba1fce1a8..aea16af0d 100644 --- a/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java +++ b/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java @@ -98,6 +98,10 @@ public final class WordListPreference extends Preference { setSummary(getSummary(status)); } + public boolean hasStatus(final int status) { + return status == mStatus; + } + @Override public View onCreateView(final ViewGroup parent) { final View orphanedView = mInterfaceState.findFirstOrphanedView(); @@ -217,6 +221,7 @@ public final class WordListPreference extends Preference { progressBar.setIds(mClientId, mWordlistId); progressBar.setMax(mFilesize); final boolean showProgressBar = (MetadataDbHelper.STATUS_DOWNLOADING == mStatus); + setSummary(getSummary(mStatus)); status.setVisibility(showProgressBar ? View.INVISIBLE : View.VISIBLE); progressBar.setVisibility(showProgressBar ? View.VISIBLE : View.INVISIBLE); diff --git a/java/src/com/android/inputmethod/event/Combiner.java b/java/src/com/android/inputmethod/event/Combiner.java index ab6b70c04..8b808c6b3 100644 --- a/java/src/com/android/inputmethod/event/Combiner.java +++ b/java/src/com/android/inputmethod/event/Combiner.java @@ -16,14 +16,33 @@ package com.android.inputmethod.event; +import java.util.ArrayList; + /** - * A generic interface for combiners. + * A generic interface for combiners. Combiners are objects that transform chains of input events + * into committable strings and manage feedback to show to the user on the combining state. */ public interface Combiner { /** - * Combine an event with the existing state and return the new event. + * Process an event, possibly combining it with the existing state and return the new event. + * + * If this event does not result in any new event getting passed down the chain, this method + * returns null. It may also modify the previous event list if appropriate. + * + * @param previousEvents the previous events in this composition. * @param event the event to combine with the existing state. * @return the resulting event. */ - Event combine(Event event); + Event processEvent(ArrayList<Event> previousEvents, Event event); + + /** + * Get the feedback that should be shown to the user for the current state of this combiner. + * @return A CharSequence representing the feedback to show users. It may include styles. + */ + CharSequence getCombiningStateFeedback(); + + /** + * Reset the state of this combiner, for example when the cursor was moved. + */ + void reset(); } diff --git a/java/src/com/android/inputmethod/event/CombinerChain.java b/java/src/com/android/inputmethod/event/CombinerChain.java new file mode 100644 index 000000000..61bc11b39 --- /dev/null +++ b/java/src/com/android/inputmethod/event/CombinerChain.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.event; + +import android.text.SpannableStringBuilder; +import android.text.TextUtils; + +import com.android.inputmethod.latin.Constants; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * This class implements the logic chain between receiving events and generating code points. + * + * Event sources are multiple. It may be a hardware keyboard, a D-PAD, a software keyboard, + * or any exotic input source. + * This class will orchestrate the composing chain that starts with an event as its input. Each + * composer will be given turns one after the other. + * The output is composed of two sequences of code points: the first, representing the already + * finished combining part, will be shown normally as the composing string, while the second is + * feedback on the composing state and will typically be shown with different styling such as + * a colored background. + */ +public class CombinerChain { + // The already combined text, as described above + private StringBuilder mCombinedText; + // The feedback on the composing state, as described above + private SpannableStringBuilder mStateFeedback; + private final ArrayList<Combiner> mCombiners; + + private static final HashMap<String, Class<? extends Combiner>> IMPLEMENTED_COMBINERS = + new HashMap<>(); + static { + IMPLEMENTED_COMBINERS.put("MyanmarReordering", MyanmarReordering.class); + } + private static final String COMBINER_SPEC_SEPARATOR = ";"; + + /** + * Create an combiner chain. + * + * The combiner chain takes events as inputs and outputs code points and combining state. + * For example, if the input language is Japanese, the combining chain will typically perform + * kana conversion. This takes a string for initial text, taken to be present before the + * cursor: we'll start after this. + * + * @param initialText The text that has already been combined so far. + * @param combinerList A list of combiners to be applied in order. + */ + public CombinerChain(final String initialText, final Combiner... combinerList) { + mCombiners = new ArrayList<>(); + // The dead key combiner is always active, and always first + mCombiners.add(new DeadKeyCombiner()); + for (final Combiner combiner : combinerList) { + mCombiners.add(combiner); + } + mCombinedText = new StringBuilder(initialText); + mStateFeedback = new SpannableStringBuilder(); + } + + public void reset() { + mCombinedText.setLength(0); + mStateFeedback.clear(); + for (final Combiner c : mCombiners) { + c.reset(); + } + } + + /** + * Pass a new event through the whole chain. + * @param previousEvents the list of previous events in this composition + * @param newEvent the new event to process + */ + public void processEvent(final ArrayList<Event> previousEvents, final Event newEvent) { + final ArrayList<Event> modifiablePreviousEvents = new ArrayList<>(previousEvents); + Event event = newEvent; + for (final Combiner combiner : mCombiners) { + // A combiner can never return more than one event; it can return several + // code points, but they should be encapsulated within one event. + event = combiner.processEvent(modifiablePreviousEvents, event); + if (null == event) { + // Combiners return null if they eat the event. + break; + } + } + if (null != event) { + // TODO: figure out the generic way of doing this + if (Constants.CODE_DELETE == event.mKeyCode) { + final int length = mCombinedText.length(); + if (length > 0) { + final int lastCodePoint = mCombinedText.codePointBefore(length); + mCombinedText.delete(length - Character.charCount(lastCodePoint), length); + } + } else { + final CharSequence textToCommit = event.getTextToCommit(); + if (!TextUtils.isEmpty(textToCommit)) { + mCombinedText.append(textToCommit); + } + } + } + mStateFeedback.clear(); + for (int i = mCombiners.size() - 1; i >= 0; --i) { + mStateFeedback.append(mCombiners.get(i).getCombiningStateFeedback()); + } + } + + /** + * Get the char sequence that should be displayed as the composing word. It may include + * styling spans. + */ + public CharSequence getComposingWordWithCombiningFeedback() { + final SpannableStringBuilder s = new SpannableStringBuilder(mCombinedText); + return s.append(mStateFeedback); + } + + public static Combiner[] createCombiners(final String spec) { + if (TextUtils.isEmpty(spec)) { + return new Combiner[0]; + } + final String[] combinerDescriptors = spec.split(COMBINER_SPEC_SEPARATOR); + final Combiner[] combiners = new Combiner[combinerDescriptors.length]; + int i = 0; + for (final String combinerDescriptor : combinerDescriptors) { + final Class<? extends Combiner> combinerClass = + IMPLEMENTED_COMBINERS.get(combinerDescriptor); + if (null == combinerClass) { + throw new RuntimeException("Unknown combiner descriptor: " + combinerDescriptor); + } + try { + combiners[i++] = combinerClass.newInstance(); + } catch (InstantiationException e) { + throw new RuntimeException("Unable to instantiate combiner: " + combinerDescriptor, + e); + } catch (IllegalAccessException e) { + throw new RuntimeException("Unable to instantiate combiner: " + combinerDescriptor, + e); + } + } + return combiners; + } +} diff --git a/java/src/com/android/inputmethod/event/DeadKeyCombiner.java b/java/src/com/android/inputmethod/event/DeadKeyCombiner.java index 52987d571..bef4d8594 100644 --- a/java/src/com/android/inputmethod/event/DeadKeyCombiner.java +++ b/java/src/com/android/inputmethod/event/DeadKeyCombiner.java @@ -21,14 +21,17 @@ import android.view.KeyCharacterMap; import com.android.inputmethod.latin.Constants; +import java.util.ArrayList; + /** * A combiner that handles dead keys. */ public class DeadKeyCombiner implements Combiner { + // TODO: make this a list of events instead final StringBuilder mDeadSequence = new StringBuilder(); @Override - public Event combine(final Event event) { + public Event processEvent(final ArrayList<Event> previousEvents, final Event event) { if (null == event) return null; // Just in case some combiner is broken if (TextUtils.isEmpty(mDeadSequence)) { if (event.isDead()) { @@ -43,19 +46,33 @@ public class DeadKeyCombiner implements Combiner { final int resultingCodePoint = KeyCharacterMap.getDeadChar(deadCodePoint, event.mCodePoint); if (0 == resultingCodePoint) { - // We can't combine both characters. We need to commit the dead key as a committable + // We can't combine both characters. We need to commit the dead key as a separate // character, and the next char too unless it's a space (because as a special case, // dead key + space should result in only the dead key being committed - that's // how dead keys work). // If the event is a space, we should commit the dead char alone, but if it's // not, we need to commit both. - return Event.createCommittableEvent(deadCodePoint, - Constants.CODE_SPACE == event.mCodePoint ? null : event /* next */); + // TODO: this is not necessarily triggered by hardware key events, so it's not + // a good idea to masquerade as one. This should be typed as a software + // composite event or something. + return Event.createHardwareKeypressEvent(deadCodePoint, event.mKeyCode, + Constants.CODE_SPACE == event.mCodePoint ? null : event /* next */, + false /* isKeyRepeat */); } else { // We could combine the characters. - return Event.createCommittableEvent(resultingCodePoint, null /* next */); + return Event.createHardwareKeypressEvent(resultingCodePoint, event.mKeyCode, + null /* next */, false /* isKeyRepeat */); } } } + @Override + public void reset() { + mDeadSequence.setLength(0); + } + + @Override + public CharSequence getCombiningStateFeedback() { + return mDeadSequence; + } } diff --git a/java/src/com/android/inputmethod/event/Event.java b/java/src/com/android/inputmethod/event/Event.java index 1f3320eb7..d257441e0 100644 --- a/java/src/com/android/inputmethod/event/Event.java +++ b/java/src/com/android/inputmethod/event/Event.java @@ -16,6 +16,10 @@ package com.android.inputmethod.event; +import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import com.android.inputmethod.latin.utils.StringUtils; + /** * Class representing a generic input event as handled by Latin IME. * @@ -32,62 +36,234 @@ public class Event { // Should the types below be represented by separate classes instead? It would be cleaner // but probably a bit too much // An event we don't handle in Latin IME, for example pressing Ctrl on a hardware keyboard. - final public static int EVENT_NOT_HANDLED = 0; - // A character that is already final, for example pressing an alphabetic character on a - // hardware qwerty keyboard. - final public static int EVENT_COMMITTABLE = 1; - // A dead key, which means a character that should combine with what is coming next. Examples - // include the "^" character on an azerty keyboard which combines with "e" to make "ê", or - // AltGr+' on a dvorak international keyboard which combines with "e" to make "é". This is - // true regardless of the language or combining mode, and should be seen as a property of the - // key - a dead key followed by another key with which it can combine should be regarded as if - // the keyboard actually had such a key. - final public static int EVENT_DEAD = 2; + final public static int EVENT_TYPE_NOT_HANDLED = 0; + // A key press that is part of input, for example pressing an alphabetic character on a + // hardware qwerty keyboard. It may be part of a sequence that will be re-interpreted later + // through combination. + final public static int EVENT_TYPE_INPUT_KEYPRESS = 1; // A toggle event is triggered by a key that affects the previous character. An example would // be a numeric key on a 10-key keyboard, which would toggle between 1 - a - b - c with // repeated presses. - final public static int EVENT_TOGGLE = 3; + final public static int EVENT_TYPE_TOGGLE = 2; // A mode event instructs the combiner to change modes. The canonical example would be the // hankaku/zenkaku key on a Japanese keyboard, or even the caps lock key on a qwerty keyboard // if handled at the combiner level. - final public static int EVENT_MODE_KEY = 4; + final public static int EVENT_TYPE_MODE_KEY = 3; + // An event corresponding to a gesture. + final public static int EVENT_TYPE_GESTURE = 4; + // An event corresponding to the manual pick of a suggestion. + final public static int EVENT_TYPE_SUGGESTION_PICKED = 5; + // An event corresponding to a string generated by some software process. + final public static int EVENT_TYPE_SOFTWARE_GENERATED_STRING = 6; + + // 0 is a valid code point, so we use -1 here. + final public static int NOT_A_CODE_POINT = -1; + // -1 is a valid key code, so we use 0 here. + final public static int NOT_A_KEY_CODE = 0; - final private static int NOT_A_CODE_POINT = 0; + final private static int FLAG_NONE = 0; + // This event is a dead character, usually input by a dead key. Examples include dead-acute + // or dead-abovering. + final private static int FLAG_DEAD = 0x1; + // This event is coming from a key repeat, software or hardware. + final private static int FLAG_REPEAT = 0x2; - final private int mType; // The type of event - one of the constants above + final private int mEventType; // The type of event - one of the constants above // The code point associated with the event, if relevant. This is a unicode code point, and // has nothing to do with other representations of the key. It is only relevant if this event - // is the right type: COMMITTABLE or DEAD or TOGGLE, but for a mode key like hankaku/zenkaku or - // ctrl, there is no code point associated so this should be NOT_A_CODE_POINT to avoid - // unintentional use of its value when it's not relevant. + // is of KEYPRESS type, but for a mode key like hankaku/zenkaku or ctrl, there is no code point + // associated so this should be NOT_A_CODE_POINT to avoid unintentional use of its value when + // it's not relevant. final public int mCodePoint; + + // If applicable, this contains the string that should be input. + final public CharSequence mText; + + // The key code associated with the event, if relevant. This is relevant whenever this event + // has been triggered by a key press, but not for a gesture for example. This has conceptually + // no link to the code point, although keys that enter a straight code point may often set + // this to be equal to mCodePoint for convenience. If this is not a key, this must contain + // NOT_A_KEY_CODE. + final public int mKeyCode; + + // Coordinates of the touch event, if relevant. If useful, we may want to replace this with + // a MotionEvent or something in the future. This is only relevant when the keypress is from + // a software keyboard obviously, unless there are touch-sensitive hardware keyboards in the + // future or some other awesome sauce. + final public int mX; + final public int mY; + + // Some flags that can't go into the key code. It's a bit field of FLAG_* + final private int mFlags; + + // If this is of type EVENT_TYPE_SUGGESTION_PICKED, this must not be null (and must be null in + // other cases). + final public SuggestedWordInfo mSuggestedWordInfo; + // The next event, if any. Null if there is no next event yet. final public Event mNextEvent; // This method is private - to create a new event, use one of the create* utility methods. - private Event(final int type, final int codePoint, final Event next) { - mType = type; + private Event(final int type, final CharSequence text, final int codePoint, final int keyCode, + final int x, final int y, final SuggestedWordInfo suggestedWordInfo, final int flags, + final Event next) { + mEventType = type; + mText = text; mCodePoint = codePoint; + mKeyCode = keyCode; + mX = x; + mY = y; + mSuggestedWordInfo = suggestedWordInfo; + mFlags = flags; mNextEvent = next; + // Sanity checks + // mSuggestedWordInfo is non-null if and only if the type is SUGGESTION_PICKED + if (EVENT_TYPE_SUGGESTION_PICKED == mEventType) { + if (null == mSuggestedWordInfo) { + throw new RuntimeException("Wrong event: SUGGESTION_PICKED event must have a " + + "non-null SuggestedWordInfo"); + } + } else { + if (null != mSuggestedWordInfo) { + throw new RuntimeException("Wrong event: only SUGGESTION_PICKED events may have " + + "a non-null SuggestedWordInfo"); + } + } + } + + public static Event createSoftwareKeypressEvent(final int codePoint, final int keyCode, + final int x, final int y, final boolean isKeyRepeat) { + return new Event(EVENT_TYPE_INPUT_KEYPRESS, null /* text */, codePoint, keyCode, x, y, + null /* suggestedWordInfo */, isKeyRepeat ? FLAG_REPEAT : FLAG_NONE, null); + } + + public static Event createHardwareKeypressEvent(final int codePoint, final int keyCode, + final Event next, final boolean isKeyRepeat) { + return new Event(EVENT_TYPE_INPUT_KEYPRESS, null /* text */, codePoint, keyCode, + Constants.EXTERNAL_KEYBOARD_COORDINATE, Constants.EXTERNAL_KEYBOARD_COORDINATE, + null /* suggestedWordInfo */, isKeyRepeat ? FLAG_REPEAT : FLAG_NONE, next); + } + + // This creates an input event for a dead character. @see {@link #FLAG_DEAD} + public static Event createDeadEvent(final int codePoint, final int keyCode, final Event next) { + // TODO: add an argument or something if we ever create a software layout with dead keys. + return new Event(EVENT_TYPE_INPUT_KEYPRESS, null /* text */, codePoint, keyCode, + Constants.EXTERNAL_KEYBOARD_COORDINATE, Constants.EXTERNAL_KEYBOARD_COORDINATE, + null /* suggestedWordInfo */, FLAG_DEAD, next); } - public static Event createDeadEvent(final int codePoint, final Event next) { - return new Event(EVENT_DEAD, codePoint, next); + /** + * Create an input event with nothing but a code point. This is the most basic possible input + * event; it contains no information on many things the IME requires to function correctly, + * so avoid using it unless really nothing is known about this input. + * @param codePoint the code point. + * @return an event for this code point. + */ + public static Event createEventForCodePointFromUnknownSource(final int codePoint) { + // TODO: should we have a different type of event for this? After all, it's not a key press. + return new Event(EVENT_TYPE_INPUT_KEYPRESS, null /* text */, codePoint, NOT_A_KEY_CODE, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, + null /* suggestedWordInfo */, FLAG_NONE, null /* next */); } - public static Event createCommittableEvent(final int codePoint, final Event next) { - return new Event(EVENT_COMMITTABLE, codePoint, next); + /** + * Creates an input event with a code point and x, y coordinates. This is typically used when + * resuming a previously-typed word, when the coordinates are still known. + * @param codePoint the code point to input. + * @param x the X coordinate. + * @param y the Y coordinate. + * @return an event for this code point and coordinates. + */ + public static Event createEventForCodePointFromAlreadyTypedText(final int codePoint, + final int x, final int y) { + // TODO: should we have a different type of event for this? After all, it's not a key press. + return new Event(EVENT_TYPE_INPUT_KEYPRESS, null /* text */, codePoint, NOT_A_KEY_CODE, + x, y, null /* suggestedWordInfo */, FLAG_NONE, null /* next */); + } + + /** + * Creates an input event representing the manual pick of a suggestion. + * @return an event for this suggestion pick. + */ + public static Event createSuggestionPickedEvent(final SuggestedWordInfo suggestedWordInfo) { + return new Event(EVENT_TYPE_SUGGESTION_PICKED, suggestedWordInfo.mWord, + NOT_A_CODE_POINT, NOT_A_KEY_CODE, + Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE, + suggestedWordInfo, FLAG_NONE, null /* next */); + } + + /** + * Creates an input event with a CharSequence. This is used by some software processes whose + * output is a string, possibly with styling. Examples include press on a multi-character key, + * or combination that outputs a string. + * @param text the CharSequence associated with this event. + * @param keyCode the key code, or NOT_A_KEYCODE if not applicable. + * @return an event for this text. + */ + public static Event createSoftwareTextEvent(final CharSequence text, final int keyCode) { + return new Event(EVENT_TYPE_SOFTWARE_GENERATED_STRING, text, NOT_A_CODE_POINT, keyCode, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, + null /* suggestedWordInfo */, FLAG_NONE, null /* next */); + } + + /** + * Creates an input event representing the manual pick of a punctuation suggestion. + * @return an event for this suggestion pick. + */ + public static Event createPunctuationSuggestionPickedEvent( + final SuggestedWordInfo suggestedWordInfo) { + final int primaryCode = suggestedWordInfo.mWord.charAt(0); + return new Event(EVENT_TYPE_SUGGESTION_PICKED, suggestedWordInfo.mWord, primaryCode, + NOT_A_KEY_CODE, Constants.SUGGESTION_STRIP_COORDINATE, + Constants.SUGGESTION_STRIP_COORDINATE, suggestedWordInfo, FLAG_NONE, + null /* next */); } public static Event createNotHandledEvent() { - return new Event(EVENT_NOT_HANDLED, NOT_A_CODE_POINT, null); + return new Event(EVENT_TYPE_NOT_HANDLED, null /* text */, NOT_A_CODE_POINT, NOT_A_KEY_CODE, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, + null /* suggestedWordInfo */, FLAG_NONE, null); } - public boolean isCommittable() { - return EVENT_COMMITTABLE == mType; + // Returns whether this is a function key like backspace, ctrl, settings... as opposed to keys + // that result in input like letters or space. + public boolean isFunctionalKeyEvent() { + // This logic may need to be refined in the future + return NOT_A_CODE_POINT == mCodePoint; } + // Returns whether this event is for a dead character. @see {@link #FLAG_DEAD} public boolean isDead() { - return EVENT_DEAD == mType; + return 0 != (FLAG_DEAD & mFlags); + } + + public boolean isKeyRepeat() { + return 0 != (FLAG_REPEAT & mFlags); + } + + // Returns whether this is a fake key press from the suggestion strip. This happens with + // punctuation signs selected from the suggestion strip. + public boolean isSuggestionStripPress() { + return EVENT_TYPE_SUGGESTION_PICKED == mEventType; + } + + public boolean isHandled() { + return EVENT_TYPE_NOT_HANDLED != mEventType; + } + + public CharSequence getTextToCommit() { + switch (mEventType) { + case EVENT_TYPE_MODE_KEY: + case EVENT_TYPE_NOT_HANDLED: + case EVENT_TYPE_TOGGLE: + return ""; + case EVENT_TYPE_INPUT_KEYPRESS: + return StringUtils.newSingleCodePointString(mCodePoint); + case EVENT_TYPE_GESTURE: + case EVENT_TYPE_SOFTWARE_GENERATED_STRING: + case EVENT_TYPE_SUGGESTION_PICKED: + return mText; + } + throw new RuntimeException("Unknown event type: " + mEventType); } } diff --git a/java/src/com/android/inputmethod/event/EventDecoderSpec.java b/java/src/com/android/inputmethod/event/EventDecoderSpec.java deleted file mode 100644 index 303b4b4c9..000000000 --- a/java/src/com/android/inputmethod/event/EventDecoderSpec.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.event; - -/** - * Class describing a decoder chain. This will depend on the language and the input medium (soft - * or hard keyboard for example). - */ -public class EventDecoderSpec { - public EventDecoderSpec() { - } -} diff --git a/java/src/com/android/inputmethod/event/EventInterpreter.java b/java/src/com/android/inputmethod/event/EventInterpreter.java deleted file mode 100644 index 726b9206b..000000000 --- a/java/src/com/android/inputmethod/event/EventInterpreter.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.event; - -import android.util.SparseArray; -import android.view.KeyEvent; - -import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.LatinIME; -import com.android.inputmethod.latin.utils.CollectionUtils; - -import java.util.ArrayList; - -/** - * This class implements the logic between receiving events and generating code points. - * - * Event sources are multiple. It may be a hardware keyboard, a D-PAD, a software keyboard, - * or any exotic input source. - * This class will orchestrate the decoding chain that starts with an event and ends up with - * a stream of code points + decoding state. - */ -public class EventInterpreter { - // TODO: Implement an object pool for events, as we'll create a lot of them - // TODO: Create a combiner - // TODO: Create an object type to represent input material + visual feedback + decoding state - // TODO: Create an interface to call back to Latin IME through the above object - - final EventDecoderSpec mDecoderSpec; - final SparseArray<HardwareEventDecoder> mHardwareEventDecoders; - final SoftwareEventDecoder mSoftwareEventDecoder; - final LatinIME mLatinIme; - final ArrayList<Combiner> mCombiners; - - /** - * Create a default interpreter. - * - * This creates a default interpreter that does nothing. A default interpreter should normally - * only be used for fallback purposes, when we really don't know what we want to do with input. - * - * @param latinIme a reference to the ime. - */ - public EventInterpreter(final LatinIME latinIme) { - this(null, latinIme); - } - - /** - * Create an event interpreter according to a specification. - * - * The specification contains information about what to do with events. Typically, it will - * contain information about the type of keyboards - for example, if hardware keyboard(s) is/are - * attached, their type will be included here so that the decoder knows what to do with each - * keypress (a 10-key keyboard is not handled like a qwerty-ish keyboard). - * It also contains information for combining characters. For example, if the input language - * is Japanese, the specification will typically request kana conversion. - * Also note that the specification can be null. This means that we need to create a default - * interpreter that does no specific combining, and assumes the most common cases. - * - * @param specification the specification for event interpretation. null for default. - * @param latinIme a reference to the ime. - */ - public EventInterpreter(final EventDecoderSpec specification, final LatinIME latinIme) { - mDecoderSpec = null != specification ? specification : new EventDecoderSpec(); - // For both, we expect to have only one decoder in almost all cases, hence the default - // capacity of 1. - mHardwareEventDecoders = new SparseArray<HardwareEventDecoder>(1); - mSoftwareEventDecoder = new SoftwareKeyboardEventDecoder(); - mCombiners = CollectionUtils.newArrayList(); - mCombiners.add(new DeadKeyCombiner()); - mLatinIme = latinIme; - } - - // Helper method to decode a hardware key event into a generic event, and execute any - // necessary action. - public boolean onHardwareKeyEvent(final KeyEvent hardwareKeyEvent) { - final Event decodedEvent = getHardwareKeyEventDecoder(hardwareKeyEvent.getDeviceId()) - .decodeHardwareKey(hardwareKeyEvent); - return onEvent(decodedEvent); - } - - public boolean onSoftwareEvent() { - final Event decodedEvent = getSoftwareEventDecoder().decodeSoftwareEvent(); - return onEvent(decodedEvent); - } - - private HardwareEventDecoder getHardwareKeyEventDecoder(final int deviceId) { - final HardwareEventDecoder decoder = mHardwareEventDecoders.get(deviceId); - if (null != decoder) return decoder; - // TODO: create the decoder according to the specification - final HardwareEventDecoder newDecoder = new HardwareKeyboardEventDecoder(deviceId); - mHardwareEventDecoders.put(deviceId, newDecoder); - return newDecoder; - } - - private SoftwareEventDecoder getSoftwareEventDecoder() { - // Within the context of Latin IME, since we never present several software interfaces - // at the time, we should never need multiple software event decoders at a time. - return mSoftwareEventDecoder; - } - - private boolean onEvent(final Event event) { - Event currentlyProcessingEvent = event; - boolean processed = false; - for (int i = 0; i < mCombiners.size(); ++i) { - currentlyProcessingEvent = mCombiners.get(i).combine(event); - } - while (null != currentlyProcessingEvent) { - if (currentlyProcessingEvent.isCommittable()) { - mLatinIme.onCodeInput(currentlyProcessingEvent.mCodePoint, - Constants.EXTERNAL_KEYBOARD_COORDINATE, - Constants.EXTERNAL_KEYBOARD_COORDINATE); - processed = true; - } else if (event.isDead()) { - processed = true; - } - currentlyProcessingEvent = currentlyProcessingEvent.mNextEvent; - } - return processed; - } -} diff --git a/java/src/com/android/inputmethod/event/HardwareKeyboardEventDecoder.java b/java/src/com/android/inputmethod/event/HardwareKeyboardEventDecoder.java index 720d07433..c61f45efa 100644 --- a/java/src/com/android/inputmethod/event/HardwareKeyboardEventDecoder.java +++ b/java/src/com/android/inputmethod/event/HardwareKeyboardEventDecoder.java @@ -46,28 +46,36 @@ public class HardwareKeyboardEventDecoder implements HardwareEventDecoder { // do not necessarily map to a unicode character. This represents a physical key, like // the key for 'A' or Space, but also Backspace or Ctrl or Caps Lock. final int keyCode = keyEvent.getKeyCode(); + final boolean isKeyRepeat = (0 != keyEvent.getRepeatCount()); if (KeyEvent.KEYCODE_DEL == keyCode) { - return Event.createCommittableEvent(Constants.CODE_DELETE, null /* next */); + return Event.createHardwareKeypressEvent(Event.NOT_A_CODE_POINT, Constants.CODE_DELETE, + null /* next */, isKeyRepeat); } if (keyEvent.isPrintingKey() || KeyEvent.KEYCODE_SPACE == keyCode || KeyEvent.KEYCODE_ENTER == keyCode) { if (0 != (codePointAndFlags & KeyCharacterMap.COMBINING_ACCENT)) { // A dead key. return Event.createDeadEvent( - codePointAndFlags & KeyCharacterMap.COMBINING_ACCENT_MASK, null /* next */); + codePointAndFlags & KeyCharacterMap.COMBINING_ACCENT_MASK, keyCode, + null /* next */); } if (KeyEvent.KEYCODE_ENTER == keyCode) { // The Enter key. If the Shift key is not being pressed, this should send a // CODE_ENTER to trigger the action if any, or a carriage return otherwise. If the // Shift key is being pressed, this should send a CODE_SHIFT_ENTER and let // Latin IME decide what to do with it. - return Event.createCommittableEvent(keyEvent.isShiftPressed() - ? Constants.CODE_SHIFT_ENTER : Constants.CODE_ENTER, - null /* next */); + if (keyEvent.isShiftPressed()) { + return Event.createHardwareKeypressEvent(Event.NOT_A_CODE_POINT, + Constants.CODE_SHIFT_ENTER, null /* next */, isKeyRepeat); + } else { + return Event.createHardwareKeypressEvent(Constants.CODE_ENTER, keyCode, + null /* next */, isKeyRepeat); + } } - // If not Enter, then we have a committable character. This should be committed - // right away, taking into account the current state. - return Event.createCommittableEvent(codePointAndFlags, null /* next */); + // If not Enter, then this is just a regular keypress event for a normal character + // that can be committed right away, taking into account the current state. + return Event.createHardwareKeypressEvent(codePointAndFlags, keyCode, null /* next */, + isKeyRepeat); } return Event.createNotHandledEvent(); } diff --git a/java/src/com/android/inputmethod/event/InputTransaction.java b/java/src/com/android/inputmethod/event/InputTransaction.java new file mode 100644 index 000000000..cdff265c6 --- /dev/null +++ b/java/src/com/android/inputmethod/event/InputTransaction.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.event; + +import com.android.inputmethod.latin.settings.SettingsValues; + +/** + * An object encapsulating a single transaction for input. + */ +public class InputTransaction { + // UPDATE_LATER is stronger than UPDATE_NOW. The reason for this is, if we have to update later, + // it's because something will change that we can't evaluate now, which means that even if we + // re-evaluate now we'll have to do it again later. The only case where that wouldn't apply + // would be if we needed to update now to find out the new state right away, but then we + // can't do it with this deferred mechanism anyway. + public static final int SHIFT_NO_UPDATE = 0; + public static final int SHIFT_UPDATE_NOW = 1; + public static final int SHIFT_UPDATE_LATER = 2; + + // Initial conditions + public final SettingsValues mSettingsValues; + public final Event mEvent; + public final long mTimestamp; + public final int mSpaceState; + public final int mShiftState; + + // Outputs + private int mRequiredShiftUpdate = SHIFT_NO_UPDATE; + private boolean mRequiresUpdateSuggestions = false; + private boolean mDidAffectContents = false; + + public InputTransaction(final SettingsValues settingsValues, final Event event, + final long timestamp, final int spaceState, final int shiftState) { + mSettingsValues = settingsValues; + mEvent = event; + mTimestamp = timestamp; + mSpaceState = spaceState; + mShiftState = shiftState; + } + + /** + * Indicate that this transaction requires some type of shift update. + * @param updateType What type of shift update this requires. + */ + public void requireShiftUpdate(final int updateType) { + mRequiredShiftUpdate = Math.max(mRequiredShiftUpdate, updateType); + } + + /** + * Gets what type of shift update this transaction requires. + * @return The shift update type. + */ + public int getRequiredShiftUpdate() { + return mRequiredShiftUpdate; + } + + /** + * Indicate that this transaction requires updating the suggestions. + */ + public void setRequiresUpdateSuggestions() { + mRequiresUpdateSuggestions = true; + } + + /** + * Find out whether this transaction requires updating the suggestions. + * @return Whether this transaction requires updating the suggestions. + */ + public boolean requiresUpdateSuggestions() { + return mRequiresUpdateSuggestions; + } + + /** + * Indicate that this transaction affected the contents of the editor. + */ + public void setDidAffectContents() { + mDidAffectContents = true; + } + + /** + * Find out whether this transaction affected contents of the editor. + * @return Whether this transaction affected contents of the editor. + */ + public boolean didAffectContents() { + return mDidAffectContents; + } +} diff --git a/java/src/com/android/inputmethod/event/MyanmarReordering.java b/java/src/com/android/inputmethod/event/MyanmarReordering.java new file mode 100644 index 000000000..32919932d --- /dev/null +++ b/java/src/com/android/inputmethod/event/MyanmarReordering.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.event; + +import com.android.inputmethod.latin.Constants; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * A combiner that reorders input for Myanmar. + */ +public class MyanmarReordering implements Combiner { + // U+1031 MYANMAR VOWEL SIGN E + private final static int VOWEL_E = 0x1031; // Code point for vowel E that we need to reorder + // U+200C ZERO WIDTH NON-JOINER + // U+200B ZERO WIDTH SPACE + private final static int ZERO_WIDTH_NON_JOINER = 0x200B; // should be 0x200C + + private final ArrayList<Event> mCurrentEvents = new ArrayList<>(); + + // List of consonants : + // U+1000 MYANMAR LETTER KA + // U+1001 MYANMAR LETTER KHA + // U+1002 MYANMAR LETTER GA + // U+1003 MYANMAR LETTER GHA + // U+1004 MYANMAR LETTER NGA + // U+1005 MYANMAR LETTER CA + // U+1006 MYANMAR LETTER CHA + // U+1007 MYANMAR LETTER JA + // U+1008 MYANMAR LETTER JHA + // U+1009 MYANMAR LETTER NYA + // U+100A MYANMAR LETTER NNYA + // U+100B MYANMAR LETTER TTA + // U+100C MYANMAR LETTER TTHA + // U+100D MYANMAR LETTER DDA + // U+100E MYANMAR LETTER DDHA + // U+100F MYANMAR LETTER NNA + // U+1010 MYANMAR LETTER TA + // U+1011 MYANMAR LETTER THA + // U+1012 MYANMAR LETTER DA + // U+1013 MYANMAR LETTER DHA + // U+1014 MYANMAR LETTER NA + // U+1015 MYANMAR LETTER PA + // U+1016 MYANMAR LETTER PHA + // U+1017 MYANMAR LETTER BA + // U+1018 MYANMAR LETTER BHA + // U+1019 MYANMAR LETTER MA + // U+101A MYANMAR LETTER YA + // U+101B MYANMAR LETTER RA + // U+101C MYANMAR LETTER LA + // U+101D MYANMAR LETTER WA + // U+101E MYANMAR LETTER SA + // U+101F MYANMAR LETTER HA + // U+1020 MYANMAR LETTER LLA + // U+103F MYANMAR LETTER GREAT SA + private static boolean isConsonant(final int codePoint) { + return (codePoint >= 0x1000 && codePoint <= 0x1020) || 0x103F == codePoint; + } + + // List of medials : + // U+103B MYANMAR CONSONANT SIGN MEDIAL YA + // U+103C MYANMAR CONSONANT SIGN MEDIAL RA + // U+103D MYANMAR CONSONANT SIGN MEDIAL WA + // U+103E MYANMAR CONSONANT SIGN MEDIAL HA + // U+105E MYANMAR CONSONANT SIGN MON MEDIAL NA + // U+105F MYANMAR CONSONANT SIGN MON MEDIAL MA + // U+1060 MYANMAR CONSONANT SIGN MON MEDIAL LA + // U+1082 MYANMAR CONSONANT SIGN SHAN MEDIAL WA + private static int[] MEDIAL_LIST = { 0x103B, 0x103C, 0x103D, 0x103E, + 0x105E, 0x105F, 0x1060, 0x1082}; + private static boolean isMedial(final int codePoint) { + return Arrays.binarySearch(MEDIAL_LIST, codePoint) >= 0; + } + + private static boolean isConsonantOrMedial(final int codePoint) { + return isConsonant(codePoint) || isMedial(codePoint); + } + + private Event getLastEvent() { + final int size = mCurrentEvents.size(); + if (size <= 0) { + return null; + } + return mCurrentEvents.get(size - 1); + } + + private CharSequence getCharSequence() { + final StringBuilder s = new StringBuilder(); + for (final Event e : mCurrentEvents) { + s.appendCodePoint(e.mCodePoint); + } + return s; + } + + /** + * Clears the currently combining stream of events and returns the resulting software text + * event corresponding to the stream. Optionally adds a new event to the cleared stream. + * @param newEvent the new event to add to the stream. null if none. + * @return the resulting software text event. Null if none. + */ + private Event clearAndGetResultingEvent(final Event newEvent) { + final CharSequence combinedText; + if (mCurrentEvents.size() > 0) { + combinedText = getCharSequence(); + mCurrentEvents.clear(); + } else { + combinedText = null; + } + if (null != newEvent) { + mCurrentEvents.add(newEvent); + } + return null == combinedText ? null + : Event.createSoftwareTextEvent(combinedText, Event.NOT_A_KEY_CODE); + } + + @Override + public Event processEvent(ArrayList<Event> previousEvents, Event newEvent) { + final int codePoint = newEvent.mCodePoint; + if (VOWEL_E == codePoint) { + final Event lastEvent = getLastEvent(); + if (null == lastEvent) { + mCurrentEvents.add(newEvent); + return null; + } else if (isConsonantOrMedial(lastEvent.mCodePoint)) { + final Event resultingEvent = clearAndGetResultingEvent(null); + mCurrentEvents.add(Event.createSoftwareKeypressEvent(ZERO_WIDTH_NON_JOINER, + Event.NOT_A_KEY_CODE, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, + false /* isKeyRepeat */)); + mCurrentEvents.add(newEvent); + return resultingEvent; + } else { // VOWEL_E == lastCodePoint. But if that was anything else this is correct too. + return clearAndGetResultingEvent(newEvent); + } + } if (isConsonant(codePoint)) { + final Event lastEvent = getLastEvent(); + if (null == lastEvent) { + mCurrentEvents.add(newEvent); + return null; + } else if (VOWEL_E == lastEvent.mCodePoint) { + final int eventSize = mCurrentEvents.size(); + if (eventSize >= 2 + && mCurrentEvents.get(eventSize - 2).mCodePoint == ZERO_WIDTH_NON_JOINER) { + // We have a ZWJN before a vowel E. We need to remove the ZWNJ and then + // reorder the vowel with respect to the consonant. + mCurrentEvents.remove(eventSize - 1); + mCurrentEvents.remove(eventSize - 2); + mCurrentEvents.add(newEvent); + mCurrentEvents.add(lastEvent); + return null; + } + // If there is already a consonant, then we are starting a new syllable. + for (int i = eventSize - 2; i >= 0; --i) { + if (isConsonant(mCurrentEvents.get(i).mCodePoint)) { + return clearAndGetResultingEvent(newEvent); + } + } + // If we come here, we didn't have a consonant so we reorder + mCurrentEvents.remove(eventSize - 1); + mCurrentEvents.add(newEvent); + mCurrentEvents.add(lastEvent); + return null; + } else { // lastCodePoint is a consonant/medial. But if it's something else it's fine + return clearAndGetResultingEvent(newEvent); + } + } else if (isMedial(codePoint)) { + final Event lastEvent = getLastEvent(); + if (null == lastEvent) { + mCurrentEvents.add(newEvent); + return null; + } else if (VOWEL_E == lastEvent.mCodePoint) { + final int eventSize = mCurrentEvents.size(); + // If there is already a consonant, then we are in the middle of a syllable, and we + // need to reorder. + boolean hasConsonant = false; + for (int i = eventSize - 2; i >= 0; --i) { + if (isConsonant(mCurrentEvents.get(i).mCodePoint)) { + hasConsonant = true; + break; + } + } + if (hasConsonant) { + mCurrentEvents.remove(eventSize - 1); + mCurrentEvents.add(newEvent); + mCurrentEvents.add(lastEvent); + return null; + } + // Otherwise, we just commit everything. + return clearAndGetResultingEvent(null); + } else { // lastCodePoint is a consonant/medial. But if it's something else it's fine + return clearAndGetResultingEvent(newEvent); + } + } else if (Constants.CODE_DELETE == newEvent.mKeyCode) { + final Event lastEvent = getLastEvent(); + final int eventSize = mCurrentEvents.size(); + if (null != lastEvent) { + if (VOWEL_E == lastEvent.mCodePoint) { + // We have a VOWEL_E at the end. There are four cases. + // - The vowel is the only code point in the buffer. Remove it. + // - The vowel is preceded by a ZWNJ. Remove both vowel E and ZWNJ. + // - The vowel is preceded by a consonant/medial, remove the consonant/medial. + // - In all other cases, it's strange, so just remove the last code point. + if (eventSize <= 1) { + mCurrentEvents.clear(); + } else { // eventSize >= 2 + final int previousCodePoint = mCurrentEvents.get(eventSize - 2).mCodePoint; + if (previousCodePoint == ZERO_WIDTH_NON_JOINER) { + mCurrentEvents.remove(eventSize - 1); + mCurrentEvents.remove(eventSize - 2); + } else if (isConsonantOrMedial(previousCodePoint)) { + mCurrentEvents.remove(eventSize - 2); + } else { + mCurrentEvents.remove(eventSize - 1); + } + } + return null; + } else if (eventSize > 0) { + mCurrentEvents.remove(eventSize - 1); + return null; + } + } + } + // This character is not part of the combining scheme, so we should reset everything. + if (mCurrentEvents.size() > 0) { + // If we have events in flight, then add the new event and return the resulting event. + mCurrentEvents.add(newEvent); + return clearAndGetResultingEvent(null); + } else { + // If we don't have any events in flight, then just pass this one through. + return newEvent; + } + } + + @Override + public CharSequence getCombiningStateFeedback() { + return getCharSequence(); + } + + @Override + public void reset() { + mCurrentEvents.clear(); + } +} diff --git a/java/src/com/android/inputmethod/event/SoftwareEventDecoder.java b/java/src/com/android/inputmethod/event/SoftwareEventDecoder.java deleted file mode 100644 index d81ee0b37..000000000 --- a/java/src/com/android/inputmethod/event/SoftwareEventDecoder.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.event; - -/** - * An event decoder for events out of a software keyboard. - * - * This defines the interface for an event decoder that supports events out of a software keyboard. - * This differs significantly from hardware keyboard event decoders in several respects. First, - * a software keyboard does not have a scancode/layout system; the keypresses that insert - * characters output unicode characters directly. - */ -public interface SoftwareEventDecoder extends EventDecoder { - public Event decodeSoftwareEvent(); -} diff --git a/java/src/com/android/inputmethod/keyboard/EmojiPalettesView.java b/java/src/com/android/inputmethod/keyboard/EmojiPalettesView.java deleted file mode 100644 index f12373503..000000000 --- a/java/src/com/android/inputmethod/keyboard/EmojiPalettesView.java +++ /dev/null @@ -1,827 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.keyboard; - -import static com.android.inputmethod.latin.Constants.NOT_A_COORDINATE; - -import android.content.Context; -import android.content.SharedPreferences; -import android.content.res.ColorStateList; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Rect; -import android.os.Build; -import android.preference.PreferenceManager; -import android.support.v4.view.PagerAdapter; -import android.support.v4.view.ViewPager; -import android.text.format.DateUtils; -import android.util.AttributeSet; -import android.util.Log; -import android.util.Pair; -import android.util.SparseArray; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TabHost; -import android.widget.TabHost.OnTabChangeListener; -import android.widget.TextView; - -import com.android.inputmethod.keyboard.internal.DynamicGridKeyboard; -import com.android.inputmethod.keyboard.internal.ScrollKeyboardView; -import com.android.inputmethod.keyboard.internal.ScrollViewWithNotifier; -import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.SubtypeSwitcher; -import com.android.inputmethod.latin.settings.Settings; -import com.android.inputmethod.latin.utils.CollectionUtils; -import com.android.inputmethod.latin.utils.ResourceUtils; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.HashMap; -import java.util.concurrent.ConcurrentHashMap; - -/** - * View class to implement Emoji palettes. - * The Emoji keyboard consists of group of views {@link R.layout#emoji_palettes_view}. - * <ol> - * <li> Emoji category tabs. - * <li> Delete button. - * <li> Emoji keyboard pages that can be scrolled by swiping horizontally or by selecting a tab. - * <li> Back to main keyboard button and enter button. - * </ol> - * Because of the above reasons, this class doesn't extend {@link KeyboardView}. - */ -public final class EmojiPalettesView extends LinearLayout implements OnTabChangeListener, - ViewPager.OnPageChangeListener, View.OnClickListener, - ScrollKeyboardView.OnKeyClickListener { - private static final String TAG = EmojiPalettesView.class.getSimpleName(); - private static final boolean DEBUG_PAGER = false; - private final int mKeyBackgroundId; - private final int mEmojiFunctionalKeyBackgroundId; - private final KeyboardLayoutSet mLayoutSet; - private final ColorStateList mTabLabelColor; - private final DeleteKeyOnTouchListener mDeleteKeyOnTouchListener; - private EmojiPalettesAdapter mEmojiPalettesAdapter; - - private TabHost mTabHost; - private ViewPager mEmojiPager; - private int mCurrentPagerPosition = 0; - private EmojiCategoryPageIndicatorView mEmojiCategoryPageIndicatorView; - - private KeyboardActionListener mKeyboardActionListener = KeyboardActionListener.EMPTY_LISTENER; - - private static final int CATEGORY_ID_UNSPECIFIED = -1; - public static final int CATEGORY_ID_RECENTS = 0; - public static final int CATEGORY_ID_PEOPLE = 1; - public static final int CATEGORY_ID_OBJECTS = 2; - public static final int CATEGORY_ID_NATURE = 3; - public static final int CATEGORY_ID_PLACES = 4; - public static final int CATEGORY_ID_SYMBOLS = 5; - public static final int CATEGORY_ID_EMOTICONS = 6; - - private static class CategoryProperties { - public int mCategoryId; - public int mPageCount; - public CategoryProperties(final int categoryId, final int pageCount) { - mCategoryId = categoryId; - mPageCount = pageCount; - } - } - - private static class EmojiCategory { - private static final String[] sCategoryName = { - "recents", - "people", - "objects", - "nature", - "places", - "symbols", - "emoticons" }; - private static final int[] sCategoryIcon = new int[] { - R.drawable.ic_emoji_recent_light, - R.drawable.ic_emoji_people_light, - R.drawable.ic_emoji_objects_light, - R.drawable.ic_emoji_nature_light, - R.drawable.ic_emoji_places_light, - R.drawable.ic_emoji_symbols_light, - 0 }; - private static final String[] sCategoryLabel = - { null, null, null, null, null, null, ":-)" }; - private static final int[] sCategoryElementId = { - KeyboardId.ELEMENT_EMOJI_RECENTS, - KeyboardId.ELEMENT_EMOJI_CATEGORY1, - KeyboardId.ELEMENT_EMOJI_CATEGORY2, - KeyboardId.ELEMENT_EMOJI_CATEGORY3, - KeyboardId.ELEMENT_EMOJI_CATEGORY4, - KeyboardId.ELEMENT_EMOJI_CATEGORY5, - KeyboardId.ELEMENT_EMOJI_CATEGORY6 }; - private final SharedPreferences mPrefs; - private final int mMaxPageKeyCount; - private final KeyboardLayoutSet mLayoutSet; - private final HashMap<String, Integer> mCategoryNameToIdMap = CollectionUtils.newHashMap(); - private final ArrayList<CategoryProperties> mShownCategories = - CollectionUtils.newArrayList(); - private final ConcurrentHashMap<Long, DynamicGridKeyboard> - mCategoryKeyboardMap = new ConcurrentHashMap<Long, DynamicGridKeyboard>(); - - private int mCurrentCategoryId = CATEGORY_ID_UNSPECIFIED; - private int mCurrentCategoryPageId = 0; - - public EmojiCategory(final SharedPreferences prefs, final Resources res, - final KeyboardLayoutSet layoutSet) { - mPrefs = prefs; - mMaxPageKeyCount = res.getInteger(R.integer.emoji_keyboard_max_key_count); - mLayoutSet = layoutSet; - for (int i = 0; i < sCategoryName.length; ++i) { - mCategoryNameToIdMap.put(sCategoryName[i], i); - } - addShownCategoryId(CATEGORY_ID_RECENTS); - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2 - || android.os.Build.VERSION.CODENAME.equalsIgnoreCase("KeyLimePie") - || android.os.Build.VERSION.CODENAME.equalsIgnoreCase("KitKat")) { - addShownCategoryId(CATEGORY_ID_PEOPLE); - addShownCategoryId(CATEGORY_ID_OBJECTS); - addShownCategoryId(CATEGORY_ID_NATURE); - addShownCategoryId(CATEGORY_ID_PLACES); - mCurrentCategoryId = - Settings.readLastShownEmojiCategoryId(mPrefs, CATEGORY_ID_PEOPLE); - } else { - mCurrentCategoryId = - Settings.readLastShownEmojiCategoryId(mPrefs, CATEGORY_ID_SYMBOLS); - } - addShownCategoryId(CATEGORY_ID_SYMBOLS); - addShownCategoryId(CATEGORY_ID_EMOTICONS); - getKeyboard(CATEGORY_ID_RECENTS, 0 /* cagetoryPageId */) - .loadRecentKeys(mCategoryKeyboardMap.values()); - } - - private void addShownCategoryId(int categoryId) { - // Load a keyboard of categoryId - getKeyboard(categoryId, 0 /* cagetoryPageId */); - final CategoryProperties properties = - new CategoryProperties(categoryId, getCategoryPageCount(categoryId)); - mShownCategories.add(properties); - } - - public String getCategoryName(int categoryId, int categoryPageId) { - return sCategoryName[categoryId] + "-" + categoryPageId; - } - - public int getCategoryId(String name) { - final String[] strings = name.split("-"); - return mCategoryNameToIdMap.get(strings[0]); - } - - public int getCategoryIcon(int categoryId) { - return sCategoryIcon[categoryId]; - } - - public String getCategoryLabel(int categoryId) { - return sCategoryLabel[categoryId]; - } - - public ArrayList<CategoryProperties> getShownCategories() { - return mShownCategories; - } - - public int getCurrentCategoryId() { - return mCurrentCategoryId; - } - - public int getCurrentCategoryPageSize() { - return getCategoryPageSize(mCurrentCategoryId); - } - - public int getCategoryPageSize(int categoryId) { - for (final CategoryProperties prop : mShownCategories) { - if (prop.mCategoryId == categoryId) { - return prop.mPageCount; - } - } - Log.w(TAG, "Invalid category id: " + categoryId); - // Should not reach here. - return 0; - } - - public void setCurrentCategoryId(int categoryId) { - mCurrentCategoryId = categoryId; - Settings.writeLastShownEmojiCategoryId(mPrefs, categoryId); - } - - public void setCurrentCategoryPageId(int id) { - mCurrentCategoryPageId = id; - } - - public int getCurrentCategoryPageId() { - return mCurrentCategoryPageId; - } - - public void saveLastTypedCategoryPage() { - Settings.writeLastTypedEmojiCategoryPageId( - mPrefs, mCurrentCategoryId, mCurrentCategoryPageId); - } - - public boolean isInRecentTab() { - return mCurrentCategoryId == CATEGORY_ID_RECENTS; - } - - public int getTabIdFromCategoryId(int categoryId) { - for (int i = 0; i < mShownCategories.size(); ++i) { - if (mShownCategories.get(i).mCategoryId == categoryId) { - return i; - } - } - Log.w(TAG, "categoryId not found: " + categoryId); - return 0; - } - - // Returns the view pager's page position for the categoryId - public int getPageIdFromCategoryId(int categoryId) { - final int lastSavedCategoryPageId = - Settings.readLastTypedEmojiCategoryPageId(mPrefs, categoryId); - int sum = 0; - for (int i = 0; i < mShownCategories.size(); ++i) { - final CategoryProperties props = mShownCategories.get(i); - if (props.mCategoryId == categoryId) { - return sum + lastSavedCategoryPageId; - } - sum += props.mPageCount; - } - Log.w(TAG, "categoryId not found: " + categoryId); - return 0; - } - - public int getRecentTabId() { - return getTabIdFromCategoryId(CATEGORY_ID_RECENTS); - } - - private int getCategoryPageCount(int categoryId) { - final Keyboard keyboard = mLayoutSet.getKeyboard(sCategoryElementId[categoryId]); - return (keyboard.getKeys().length - 1) / mMaxPageKeyCount + 1; - } - - // Returns a pair of the category id and the category page id from the view pager's page - // position. The category page id is numbered in each category. And the view page position - // is the position of the current shown page in the view pager which contains all pages of - // all categories. - public Pair<Integer, Integer> getCategoryIdAndPageIdFromPagePosition(int position) { - int sum = 0; - for (CategoryProperties properties : mShownCategories) { - final int temp = sum; - sum += properties.mPageCount; - if (sum > position) { - return new Pair<Integer, Integer>(properties.mCategoryId, position - temp); - } - } - return null; - } - - // Returns a keyboard from the view pager's page position. - public DynamicGridKeyboard getKeyboardFromPagePosition(int position) { - final Pair<Integer, Integer> categoryAndId = - getCategoryIdAndPageIdFromPagePosition(position); - if (categoryAndId != null) { - return getKeyboard(categoryAndId.first, categoryAndId.second); - } - return null; - } - - public DynamicGridKeyboard getKeyboard(int categoryId, int id) { - synchronized(mCategoryKeyboardMap) { - final long key = (((long) categoryId) << Constants.MAX_INT_BIT_COUNT) | id; - final DynamicGridKeyboard kbd; - if (!mCategoryKeyboardMap.containsKey(key)) { - if (categoryId != CATEGORY_ID_RECENTS) { - final Keyboard keyboard = - mLayoutSet.getKeyboard(sCategoryElementId[categoryId]); - final Key[][] sortedKeys = sortKeys(keyboard.getKeys(), mMaxPageKeyCount); - for (int i = 0; i < sortedKeys.length; ++i) { - final DynamicGridKeyboard tempKbd = new DynamicGridKeyboard(mPrefs, - mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS), - mMaxPageKeyCount, categoryId, i /* categoryPageId */); - for (Key emojiKey : sortedKeys[i]) { - if (emojiKey == null) { - break; - } - tempKbd.addKeyLast(emojiKey); - } - mCategoryKeyboardMap.put((((long) categoryId) - << Constants.MAX_INT_BIT_COUNT) | i, tempKbd); - } - kbd = mCategoryKeyboardMap.get(key); - } else { - kbd = new DynamicGridKeyboard(mPrefs, - mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS), - mMaxPageKeyCount, categoryId, 0 /* categoryPageId */); - mCategoryKeyboardMap.put(key, kbd); - } - } else { - kbd = mCategoryKeyboardMap.get(key); - } - return kbd; - } - } - - public int getTotalPageCountOfAllCategories() { - int sum = 0; - for (CategoryProperties properties : mShownCategories) { - sum += properties.mPageCount; - } - return sum; - } - - private Key[][] sortKeys(Key[] inKeys, int maxPageCount) { - Key[] keys = Arrays.copyOf(inKeys, inKeys.length); - Arrays.sort(keys, 0, keys.length, new Comparator<Key>() { - @Override - public int compare(Key lhs, Key rhs) { - final Rect lHitBox = lhs.getHitBox(); - final Rect rHitBox = rhs.getHitBox(); - if (lHitBox.top < rHitBox.top) { - return -1; - } else if (lHitBox.top > rHitBox.top) { - return 1; - } - if (lHitBox.left < rHitBox.left) { - return -1; - } else if (lHitBox.left > rHitBox.left) { - return 1; - } - if (lhs.getCode() == rhs.getCode()) { - return 0; - } - return lhs.getCode() < rhs.getCode() ? -1 : 1; - } - }); - final int pageCount = (keys.length - 1) / maxPageCount + 1; - final Key[][] retval = new Key[pageCount][maxPageCount]; - for (int i = 0; i < keys.length; ++i) { - retval[i / maxPageCount][i % maxPageCount] = keys[i]; - } - return retval; - } - } - - private final EmojiCategory mEmojiCategory; - - public EmojiPalettesView(final Context context, final AttributeSet attrs) { - this(context, attrs, R.attr.emojiPalettesViewStyle); - } - - public EmojiPalettesView(final Context context, final AttributeSet attrs, final int defStyle) { - super(context, attrs, defStyle); - final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs, - R.styleable.KeyboardView, defStyle, R.style.KeyboardView); - mKeyBackgroundId = keyboardViewAttr.getResourceId( - R.styleable.KeyboardView_keyBackground, 0); - mEmojiFunctionalKeyBackgroundId = keyboardViewAttr.getResourceId( - R.styleable.KeyboardView_keyBackgroundEmojiFunctional, 0); - keyboardViewAttr.recycle(); - final TypedArray emojiPalettesViewAttr = context.obtainStyledAttributes(attrs, - R.styleable.EmojiPalettesView, defStyle, R.style.EmojiPalettesView); - mTabLabelColor = emojiPalettesViewAttr.getColorStateList( - R.styleable.EmojiPalettesView_emojiTabLabelColor); - emojiPalettesViewAttr.recycle(); - final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder( - context, null /* editorInfo */); - final Resources res = context.getResources(); - final EmojiLayoutParams emojiLp = new EmojiLayoutParams(res); - builder.setSubtype(SubtypeSwitcher.getInstance().getEmojiSubtype()); - builder.setKeyboardGeometry(ResourceUtils.getDefaultKeyboardWidth(res), - emojiLp.mEmojiKeyboardHeight); - builder.setOptions(false, false, false /* lanuageSwitchKeyEnabled */); - mLayoutSet = builder.build(); - mEmojiCategory = new EmojiCategory(PreferenceManager.getDefaultSharedPreferences(context), - context.getResources(), builder.build()); - mDeleteKeyOnTouchListener = new DeleteKeyOnTouchListener(context); - } - - @Override - protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - final Resources res = getContext().getResources(); - // The main keyboard expands to the entire this {@link KeyboardView}. - final int width = ResourceUtils.getDefaultKeyboardWidth(res) - + getPaddingLeft() + getPaddingRight(); - final int height = ResourceUtils.getDefaultKeyboardHeight(res) - + res.getDimensionPixelSize(R.dimen.suggestions_strip_height) - + getPaddingTop() + getPaddingBottom(); - setMeasuredDimension(width, height); - } - - private void addTab(final TabHost host, final int categoryId) { - final String tabId = mEmojiCategory.getCategoryName(categoryId, 0 /* categoryPageId */); - final TabHost.TabSpec tspec = host.newTabSpec(tabId); - tspec.setContent(R.id.emoji_keyboard_dummy); - if (mEmojiCategory.getCategoryIcon(categoryId) != 0) { - final ImageView iconView = (ImageView)LayoutInflater.from(getContext()).inflate( - R.layout.emoji_keyboard_tab_icon, null); - iconView.setImageResource(mEmojiCategory.getCategoryIcon(categoryId)); - tspec.setIndicator(iconView); - } - if (mEmojiCategory.getCategoryLabel(categoryId) != null) { - final TextView textView = (TextView)LayoutInflater.from(getContext()).inflate( - R.layout.emoji_keyboard_tab_label, null); - textView.setText(mEmojiCategory.getCategoryLabel(categoryId)); - textView.setTextColor(mTabLabelColor); - tspec.setIndicator(textView); - } - host.addTab(tspec); - } - - @Override - protected void onFinishInflate() { - mTabHost = (TabHost)findViewById(R.id.emoji_category_tabhost); - mTabHost.setup(); - for (final CategoryProperties properties : mEmojiCategory.getShownCategories()) { - addTab(mTabHost, properties.mCategoryId); - } - mTabHost.setOnTabChangedListener(this); - mTabHost.getTabWidget().setStripEnabled(true); - - mEmojiPalettesAdapter = new EmojiPalettesAdapter(mEmojiCategory, mLayoutSet, this); - - mEmojiPager = (ViewPager)findViewById(R.id.emoji_keyboard_pager); - mEmojiPager.setAdapter(mEmojiPalettesAdapter); - mEmojiPager.setOnPageChangeListener(this); - mEmojiPager.setOffscreenPageLimit(0); - mEmojiPager.setPersistentDrawingCache(ViewPager.PERSISTENT_NO_CACHE); - final Resources res = getResources(); - final EmojiLayoutParams emojiLp = new EmojiLayoutParams(res); - emojiLp.setPagerProperties(mEmojiPager); - - mEmojiCategoryPageIndicatorView = - (EmojiCategoryPageIndicatorView)findViewById(R.id.emoji_category_page_id_view); - emojiLp.setCategoryPageIdViewProperties(mEmojiCategoryPageIndicatorView); - - setCurrentCategoryId(mEmojiCategory.getCurrentCategoryId(), true /* force */); - - final LinearLayout actionBar = (LinearLayout)findViewById(R.id.emoji_action_bar); - emojiLp.setActionBarProperties(actionBar); - - final ImageView deleteKey = (ImageView)findViewById(R.id.emoji_keyboard_delete); - deleteKey.setTag(Constants.CODE_DELETE); - deleteKey.setOnTouchListener(mDeleteKeyOnTouchListener); - final ImageView alphabetKey = (ImageView)findViewById(R.id.emoji_keyboard_alphabet); - alphabetKey.setBackgroundResource(mEmojiFunctionalKeyBackgroundId); - alphabetKey.setTag(Constants.CODE_SWITCH_ALPHA_SYMBOL); - alphabetKey.setOnClickListener(this); - final ImageView spaceKey = (ImageView)findViewById(R.id.emoji_keyboard_space); - spaceKey.setBackgroundResource(mKeyBackgroundId); - spaceKey.setTag(Constants.CODE_SPACE); - spaceKey.setOnClickListener(this); - emojiLp.setKeyProperties(spaceKey); - final ImageView alphabetKey2 = (ImageView)findViewById(R.id.emoji_keyboard_alphabet2); - alphabetKey2.setBackgroundResource(mEmojiFunctionalKeyBackgroundId); - alphabetKey2.setTag(Constants.CODE_SWITCH_ALPHA_SYMBOL); - alphabetKey2.setOnClickListener(this); - } - - @Override - public void onTabChanged(final String tabId) { - final int categoryId = mEmojiCategory.getCategoryId(tabId); - setCurrentCategoryId(categoryId, false /* force */); - updateEmojiCategoryPageIdView(); - } - - - @Override - public void onPageSelected(final int position) { - final Pair<Integer, Integer> newPos = - mEmojiCategory.getCategoryIdAndPageIdFromPagePosition(position); - setCurrentCategoryId(newPos.first /* categoryId */, false /* force */); - mEmojiCategory.setCurrentCategoryPageId(newPos.second /* categoryPageId */); - updateEmojiCategoryPageIdView(); - mCurrentPagerPosition = position; - } - - @Override - public void onPageScrollStateChanged(final int state) { - // Ignore this message. Only want the actual page selected. - } - - @Override - public void onPageScrolled(final int position, final float positionOffset, - final int positionOffsetPixels) { - final Pair<Integer, Integer> newPos = - mEmojiCategory.getCategoryIdAndPageIdFromPagePosition(position); - final int newCategoryId = newPos.first; - final int newCategorySize = mEmojiCategory.getCategoryPageSize(newCategoryId); - final int currentCategoryId = mEmojiCategory.getCurrentCategoryId(); - final int currentCategoryPageId = mEmojiCategory.getCurrentCategoryPageId(); - final int currentCategorySize = mEmojiCategory.getCurrentCategoryPageSize(); - if (newCategoryId == currentCategoryId) { - mEmojiCategoryPageIndicatorView.setCategoryPageId( - newCategorySize, newPos.second, positionOffset); - } else if (newCategoryId > currentCategoryId) { - mEmojiCategoryPageIndicatorView.setCategoryPageId( - currentCategorySize, currentCategoryPageId, positionOffset); - } else if (newCategoryId < currentCategoryId) { - mEmojiCategoryPageIndicatorView.setCategoryPageId( - currentCategorySize, currentCategoryPageId, positionOffset - 1); - } - } - - @Override - public void onClick(final View v) { - if (v.getTag() instanceof Integer) { - final int code = (Integer)v.getTag(); - registerCode(code); - return; - } - } - - private void registerCode(final int code) { - mKeyboardActionListener.onPressKey(code, 0 /* repeatCount */, true /* isSinglePointer */); - mKeyboardActionListener.onCodeInput(code, NOT_A_COORDINATE, NOT_A_COORDINATE); - mKeyboardActionListener.onReleaseKey(code, false /* withSliding */); - } - - @Override - public void onKeyClick(final Key key) { - mEmojiPalettesAdapter.addRecentKey(key); - mEmojiCategory.saveLastTypedCategoryPage(); - final int code = key.getCode(); - if (code == Constants.CODE_OUTPUT_TEXT) { - mKeyboardActionListener.onTextInput(key.getOutputText()); - return; - } - registerCode(code); - } - - public void setHardwareAcceleratedDrawingEnabled(final boolean enabled) { - // TODO: - } - - public void startEmojiPalettes() { - if (DEBUG_PAGER) { - Log.d(TAG, "allocate emoji palettes memory " + mCurrentPagerPosition); - } - mEmojiPager.setAdapter(mEmojiPalettesAdapter); - mEmojiPager.setCurrentItem(mCurrentPagerPosition); - } - - public void stopEmojiPalettes() { - if (DEBUG_PAGER) { - Log.d(TAG, "deallocate emoji palettes memory"); - } - mEmojiPalettesAdapter.flushPendingRecentKeys(); - mEmojiPager.setAdapter(null); - } - - public void setKeyboardActionListener(final KeyboardActionListener listener) { - mKeyboardActionListener = listener; - mDeleteKeyOnTouchListener.setKeyboardActionListener(mKeyboardActionListener); - } - - private void updateEmojiCategoryPageIdView() { - if (mEmojiCategoryPageIndicatorView == null) { - return; - } - mEmojiCategoryPageIndicatorView.setCategoryPageId( - mEmojiCategory.getCurrentCategoryPageSize(), - mEmojiCategory.getCurrentCategoryPageId(), 0.0f /* offset */); - } - - private void setCurrentCategoryId(final int categoryId, final boolean force) { - final int oldCategoryId = mEmojiCategory.getCurrentCategoryId(); - if (oldCategoryId == categoryId && !force) { - return; - } - - if (oldCategoryId == CATEGORY_ID_RECENTS) { - // Needs to save pending updates for recent keys when we get out of the recents - // category because we don't want to move the recent emojis around while the user - // is in the recents category. - mEmojiPalettesAdapter.flushPendingRecentKeys(); - } - - mEmojiCategory.setCurrentCategoryId(categoryId); - final int newTabId = mEmojiCategory.getTabIdFromCategoryId(categoryId); - final int newCategoryPageId = mEmojiCategory.getPageIdFromCategoryId(categoryId); - if (force || mEmojiCategory.getCategoryIdAndPageIdFromPagePosition( - mEmojiPager.getCurrentItem()).first != categoryId) { - mEmojiPager.setCurrentItem(newCategoryPageId, false /* smoothScroll */); - } - if (force || mTabHost.getCurrentTab() != newTabId) { - mTabHost.setCurrentTab(newTabId); - } - } - - private static class EmojiPalettesAdapter extends PagerAdapter { - private final ScrollKeyboardView.OnKeyClickListener mListener; - private final DynamicGridKeyboard mRecentsKeyboard; - private final SparseArray<ScrollKeyboardView> mActiveKeyboardViews = - CollectionUtils.newSparseArray(); - private final EmojiCategory mEmojiCategory; - private int mActivePosition = 0; - - public EmojiPalettesAdapter(final EmojiCategory emojiCategory, - final KeyboardLayoutSet layoutSet, - final ScrollKeyboardView.OnKeyClickListener listener) { - mEmojiCategory = emojiCategory; - mListener = listener; - mRecentsKeyboard = mEmojiCategory.getKeyboard(CATEGORY_ID_RECENTS, 0); - } - - public void flushPendingRecentKeys() { - mRecentsKeyboard.flushPendingRecentKeys(); - final KeyboardView recentKeyboardView = - mActiveKeyboardViews.get(mEmojiCategory.getRecentTabId()); - if (recentKeyboardView != null) { - recentKeyboardView.invalidateAllKeys(); - } - } - - public void addRecentKey(final Key key) { - if (mEmojiCategory.isInRecentTab()) { - mRecentsKeyboard.addPendingKey(key); - return; - } - mRecentsKeyboard.addKeyFirst(key); - final KeyboardView recentKeyboardView = - mActiveKeyboardViews.get(mEmojiCategory.getRecentTabId()); - if (recentKeyboardView != null) { - recentKeyboardView.invalidateAllKeys(); - } - } - - @Override - public int getCount() { - return mEmojiCategory.getTotalPageCountOfAllCategories(); - } - - @Override - public void setPrimaryItem(final View container, final int position, final Object object) { - if (mActivePosition == position) { - return; - } - final ScrollKeyboardView oldKeyboardView = mActiveKeyboardViews.get(mActivePosition); - if (oldKeyboardView != null) { - oldKeyboardView.releaseCurrentKey(); - oldKeyboardView.deallocateMemory(); - } - mActivePosition = position; - } - - @Override - public Object instantiateItem(final ViewGroup container, final int position) { - if (DEBUG_PAGER) { - Log.d(TAG, "instantiate item: " + position); - } - final ScrollKeyboardView oldKeyboardView = mActiveKeyboardViews.get(position); - if (oldKeyboardView != null) { - oldKeyboardView.deallocateMemory(); - // This may be redundant but wanted to be safer.. - mActiveKeyboardViews.remove(position); - } - final Keyboard keyboard = - mEmojiCategory.getKeyboardFromPagePosition(position); - final LayoutInflater inflater = LayoutInflater.from(container.getContext()); - final View view = inflater.inflate( - R.layout.emoji_keyboard_page, container, false /* attachToRoot */); - final ScrollKeyboardView keyboardView = (ScrollKeyboardView)view.findViewById( - R.id.emoji_keyboard_page); - keyboardView.setKeyboard(keyboard); - keyboardView.setOnKeyClickListener(mListener); - final ScrollViewWithNotifier scrollView = (ScrollViewWithNotifier)view.findViewById( - R.id.emoji_keyboard_scroller); - keyboardView.setScrollView(scrollView); - container.addView(view); - mActiveKeyboardViews.put(position, keyboardView); - return view; - } - - @Override - public boolean isViewFromObject(final View view, final Object object) { - return view == object; - } - - @Override - public void destroyItem(final ViewGroup container, final int position, - final Object object) { - if (DEBUG_PAGER) { - Log.d(TAG, "destroy item: " + position + ", " + object.getClass().getSimpleName()); - } - final ScrollKeyboardView keyboardView = mActiveKeyboardViews.get(position); - if (keyboardView != null) { - keyboardView.deallocateMemory(); - mActiveKeyboardViews.remove(position); - } - if (object instanceof View) { - container.removeView((View)object); - } else { - Log.w(TAG, "Warning!!! Emoji palette may be leaking. " + object); - } - } - } - - // TODO: Do the same things done in PointerTracker - private static class DeleteKeyOnTouchListener implements OnTouchListener { - private static final long MAX_REPEAT_COUNT_TIME = 30 * DateUtils.SECOND_IN_MILLIS; - private final int mDeleteKeyPressedBackgroundColor; - private final long mKeyRepeatStartTimeout; - private final long mKeyRepeatInterval; - - public DeleteKeyOnTouchListener(Context context) { - final Resources res = context.getResources(); - mDeleteKeyPressedBackgroundColor = - res.getColor(R.color.emoji_key_pressed_background_color); - mKeyRepeatStartTimeout = res.getInteger(R.integer.config_key_repeat_start_timeout); - mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval); - } - - private KeyboardActionListener mKeyboardActionListener = - KeyboardActionListener.EMPTY_LISTENER; - private DummyRepeatKeyRepeatTimer mTimer; - - private synchronized void startRepeat() { - if (mTimer != null) { - abortRepeat(); - } - mTimer = new DummyRepeatKeyRepeatTimer(); - mTimer.start(); - } - - private synchronized void abortRepeat() { - mTimer.abort(); - mTimer = null; - } - - // TODO: Remove - // This function is mimicking the repeat code in PointerTracker. - // Specifically referring to PointerTracker#startRepeatKey and PointerTracker#onKeyRepeat. - private class DummyRepeatKeyRepeatTimer extends Thread { - public boolean mAborted = false; - - @Override - public void run() { - int repeatCount = 1; - int timeCount = 0; - while (timeCount < MAX_REPEAT_COUNT_TIME && !mAborted) { - if (timeCount > mKeyRepeatStartTimeout) { - pressDelete(repeatCount); - } - timeCount += mKeyRepeatInterval; - ++repeatCount; - try { - Thread.sleep(mKeyRepeatInterval); - } catch (InterruptedException e) { - } - } - } - - public void abort() { - mAborted = true; - } - } - - public void pressDelete(int repeatCount) { - mKeyboardActionListener.onPressKey( - Constants.CODE_DELETE, repeatCount, true /* isSinglePointer */); - mKeyboardActionListener.onCodeInput( - Constants.CODE_DELETE, NOT_A_COORDINATE, NOT_A_COORDINATE); - mKeyboardActionListener.onReleaseKey( - Constants.CODE_DELETE, false /* withSliding */); - } - - public void setKeyboardActionListener(KeyboardActionListener listener) { - mKeyboardActionListener = listener; - } - - @Override - public boolean onTouch(View v, MotionEvent event) { - switch(event.getAction()) { - case MotionEvent.ACTION_DOWN: - v.setBackgroundColor(mDeleteKeyPressedBackgroundColor); - pressDelete(0 /* repeatCount */); - startRepeat(); - return true; - case MotionEvent.ACTION_UP: - v.setBackgroundColor(0); - abortRepeat(); - return true; - } - return false; - } - } -} diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java index f7ec9509d..ed3b2b347 100644 --- a/java/src/com/android/inputmethod/keyboard/Key.java +++ b/java/src/com/android/inputmethod/keyboard/Key.java @@ -22,14 +22,11 @@ import static com.android.inputmethod.latin.Constants.CODE_SHIFT; import static com.android.inputmethod.latin.Constants.CODE_SWITCH_ALPHA_SYMBOL; import static com.android.inputmethod.latin.Constants.CODE_UNSPECIFIED; -import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.text.TextUtils; -import android.util.Log; -import android.util.Xml; import com.android.inputmethod.keyboard.internal.KeyDrawParams; import com.android.inputmethod.keyboard.internal.KeySpecParser; @@ -43,9 +40,6 @@ import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.utils.StringUtils; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - import java.util.Arrays; import java.util.Locale; @@ -53,8 +47,6 @@ import java.util.Locale; * Class for describing the position and characteristics of a single key in the keyboard. */ public class Key implements Comparable<Key> { - private static final String TAG = Key.class.getSimpleName(); - /** * The key code (unicode or custom code) that this key generates. */ @@ -68,9 +60,13 @@ public class Key implements Comparable<Key> { private final int mLabelFlags; private static final int LABEL_FLAGS_ALIGN_LEFT = 0x01; private static final int LABEL_FLAGS_ALIGN_RIGHT = 0x02; + private static final int LABEL_FLAGS_ALIGN_BUTTOM = 0x04; private static final int LABEL_FLAGS_ALIGN_LEFT_OF_CENTER = 0x08; + // Font typeface specification. + private static final int LABEL_FLAGS_FONT_MASK = 0x30; private static final int LABEL_FLAGS_FONT_NORMAL = 0x10; private static final int LABEL_FLAGS_FONT_MONO_SPACE = 0x20; + private static final int LABEL_FLAGS_FONT_DEFAULT = 0x30; // Start of key text ratio enum values private static final int LABEL_FLAGS_FOLLOW_KEY_TEXT_RATIO_MASK = 0x1C0; private static final int LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO = 0x40; @@ -84,10 +80,17 @@ public class Key implements Comparable<Key> { private static final int LABEL_FLAGS_HAS_HINT_LABEL = 0x800; private static final int LABEL_FLAGS_WITH_ICON_LEFT = 0x1000; private static final int LABEL_FLAGS_WITH_ICON_RIGHT = 0x2000; + // The bit to calculate the ratio of key label width against key width. If autoXScale bit is on + // and autoYScale bit is off, the key label may be shrunk only for X-direction. + // If both autoXScale and autoYScale bits are on, the key label text size may be auto scaled. private static final int LABEL_FLAGS_AUTO_X_SCALE = 0x4000; - private static final int LABEL_FLAGS_PRESERVE_CASE = 0x8000; - private static final int LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED = 0x10000; - private static final int LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL = 0x20000; + private static final int LABEL_FLAGS_AUTO_Y_SCALE = 0x8000; + private static final int LABEL_FLAGS_AUTO_SCALE = LABEL_FLAGS_AUTO_X_SCALE + | LABEL_FLAGS_AUTO_Y_SCALE; + private static final int LABEL_FLAGS_PRESERVE_CASE = 0x10000; + private static final int LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED = 0x20000; + private static final int LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL = 0x40000; + private static final int LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR = 0x80000; private static final int LABEL_FLAGS_DISABLE_HINT_LABEL = 0x40000000; private static final int LABEL_FLAGS_DISABLE_ADDITIONAL_MORE_KEYS = 0x80000000; @@ -139,8 +142,6 @@ public class Key implements Comparable<Key> { private final OptionalAttributes mOptionalAttributes; - private static final int DEFAULT_TEXT_COLOR = 0xFFFFFFFF; - private static final class OptionalAttributes { /** Text to output when pressed. This can be multiple characters, like ".com" */ public final String mOutputText; @@ -185,26 +186,20 @@ public class Key implements Comparable<Key> { private boolean mEnabled = true; /** - * This constructor is being used only for keys in more keys keyboard. + * Constructor for a key on <code>MoreKeyKeyboard</code>, on <code>MoreSuggestions</code>, + * and in a <GridRows/>. */ - public Key(final KeyboardParams params, final MoreKeySpec moreKeySpec, final int x, final int y, - final int width, final int height, final int labelFlags) { - this(params, moreKeySpec.mLabel, null, moreKeySpec.mIconId, moreKeySpec.mCode, - moreKeySpec.mOutputText, x, y, width, height, labelFlags, BACKGROUND_TYPE_NORMAL); - } - - /** - * This constructor is being used only for key in popup suggestions pane. - */ - public Key(final KeyboardParams params, final String label, final String hintLabel, - final int iconId, final int code, final String outputText, final int x, final int y, - final int width, final int height, final int labelFlags, final int backgroundType) { - mHeight = height - params.mVerticalGap; - mWidth = width - params.mHorizontalGap; + public Key(final String label, final int iconId, final int code, final String outputText, + final String hintLabel, final int labelFlags, final int backgroundType, final int x, + final int y, final int width, final int height, final int horizontalGap, + final int verticalGap) { + mHeight = height - verticalGap; + mWidth = width - horizontalGap; mHintLabel = hintLabel; mLabelFlags = labelFlags; mBackgroundType = backgroundType; - mActionFlags = 0; + // TODO: Pass keyActionFlags as an argument. + mActionFlags = ACTION_FLAGS_NO_KEY_PREVIEW; mMoreKeys = null; mMoreKeysColumnAndFlags = 0; mLabel = label; @@ -215,7 +210,7 @@ public class Key implements Comparable<Key> { mEnabled = (code != CODE_UNSPECIFIED); mIconId = iconId; // Horizontal gap is divided equally to both sides of the key. - mX = x + params.mHorizontalGap / 2; + mX = x + horizontalGap / 2; mY = y; mHitBox.set(x, y, x + width + 1, y + height); mKeyVisualAttributes = null; @@ -224,25 +219,22 @@ public class Key implements Comparable<Key> { } /** - * Create a key with the given top-left coordinate and extract its attributes from the XML - * parser. - * @param res resources associated with the caller's context + * Create a key with the given top-left coordinate and extract its attributes from a key + * specification string, Key attribute array, key style, and etc. + * + * @param keySpec the key specification. + * @param keyAttr the Key XML attributes array. + * @param style the {@link KeyStyle} of this key. * @param params the keyboard building parameters. * @param row the row that this key belongs to. row's x-coordinate will be the right edge of * this key. - * @param parser the XML parser containing the attributes for this key - * @throws XmlPullParserException */ - public Key(final Resources res, final KeyboardParams params, final KeyboardRow row, - final XmlPullParser parser) throws XmlPullParserException { + public Key(final String keySpec, final TypedArray keyAttr, final KeyStyle style, + final KeyboardParams params, final KeyboardRow row) { final float horizontalGap = isSpacer() ? 0 : params.mHorizontalGap; final int rowHeight = row.getRowHeight(); mHeight = rowHeight - params.mVerticalGap; - final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser), - R.styleable.Keyboard_Key); - - final KeyStyle style = params.mKeyStyles.getKeyStyle(keyAttr, parser); final float keyXPos = row.getKeyX(keyAttr); final float keyWidth = row.getKeyWidth(keyAttr, keyXPos); final int keyYPos = row.getKeyY(); @@ -264,12 +256,6 @@ public class Key implements Comparable<Key> { R.styleable.Keyboard_Key_visualInsetsLeft, baseWidth, baseWidth, 0)); final int visualInsetsRight = Math.round(keyAttr.getFraction( R.styleable.Keyboard_Key_visualInsetsRight, baseWidth, baseWidth, 0)); - mIconId = KeySpecParser.getIconId(style.getString(keyAttr, - R.styleable.Keyboard_Key_keyIcon)); - final int disabledIconId = KeySpecParser.getIconId(style.getString(keyAttr, - R.styleable.Keyboard_Key_keyIconDisabled)); - final int previewIconId = KeySpecParser.getIconId(style.getString(keyAttr, - R.styleable.Keyboard_Key_keyIconPreview)); mLabelFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags) | row.getDefaultKeyLabelFlags(); @@ -281,19 +267,19 @@ public class Key implements Comparable<Key> { int moreKeysColumn = style.getInt(keyAttr, R.styleable.Keyboard_Key_maxMoreKeysColumn, params.mMaxMoreKeysKeyboardColumn); int value; - if ((value = KeySpecParser.getIntValue(moreKeys, MORE_KEYS_AUTO_COLUMN_ORDER, -1)) > 0) { + if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_AUTO_COLUMN_ORDER, -1)) > 0) { moreKeysColumn = value & MORE_KEYS_COLUMN_MASK; } - if ((value = KeySpecParser.getIntValue(moreKeys, MORE_KEYS_FIXED_COLUMN_ORDER, -1)) > 0) { + if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_FIXED_COLUMN_ORDER, -1)) > 0) { moreKeysColumn = MORE_KEYS_FLAGS_FIXED_COLUMN_ORDER | (value & MORE_KEYS_COLUMN_MASK); } - if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_HAS_LABELS)) { + if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_HAS_LABELS)) { moreKeysColumn |= MORE_KEYS_FLAGS_HAS_LABELS; } - if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_NEEDS_DIVIDERS)) { + if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NEEDS_DIVIDERS)) { moreKeysColumn |= MORE_KEYS_FLAGS_NEEDS_DIVIDERS; } - if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_NO_PANEL_AUTO_MORE_KEY)) { + if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NO_PANEL_AUTO_MORE_KEY)) { moreKeysColumn |= MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY; } mMoreKeysColumnAndFlags = moreKeysColumn; @@ -305,21 +291,25 @@ public class Key implements Comparable<Key> { additionalMoreKeys = style.getStringArray(keyAttr, R.styleable.Keyboard_Key_additionalMoreKeys); } - moreKeys = KeySpecParser.insertAdditionalMoreKeys(moreKeys, additionalMoreKeys); + moreKeys = MoreKeySpec.insertAdditionalMoreKeys(moreKeys, additionalMoreKeys); if (moreKeys != null) { actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS; mMoreKeys = new MoreKeySpec[moreKeys.length]; for (int i = 0; i < moreKeys.length; i++) { - mMoreKeys[i] = new MoreKeySpec( - moreKeys[i], needsToUpperCase, locale, params.mCodesSet); + mMoreKeys[i] = new MoreKeySpec(moreKeys[i], needsToUpperCase, locale); } } else { mMoreKeys = null; } mActionFlags = actionFlags; - final int code = KeySpecParser.parseCode(style.getString(keyAttr, - R.styleable.Keyboard_Key_code), params.mCodesSet, CODE_UNSPECIFIED); + mIconId = KeySpecParser.getIconId(keySpec); + final int disabledIconId = KeySpecParser.getIconId(style.getString(keyAttr, + R.styleable.Keyboard_Key_keyIconDisabled)); + final int previewIconId = KeySpecParser.getIconId(style.getString(keyAttr, + R.styleable.Keyboard_Key_keyIconPreview)); + + final int code = KeySpecParser.getCode(keySpec); if ((mLabelFlags & LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL) != 0) { mLabel = params.mId.mCustomActionLabel; } else if (code >= Character.MIN_SUPPLEMENTARY_CODE_POINT) { @@ -328,25 +318,24 @@ public class Key implements Comparable<Key> { // code point nor as a surrogate pair. mLabel = new StringBuilder().appendCodePoint(code).toString(); } else { - mLabel = KeySpecParser.toUpperCaseOfStringForLocale(style.getString(keyAttr, - R.styleable.Keyboard_Key_keyLabel), needsToUpperCase, locale); + mLabel = StringUtils.toUpperCaseOfStringForLocale( + KeySpecParser.getLabel(keySpec), needsToUpperCase, locale); } if ((mLabelFlags & LABEL_FLAGS_DISABLE_HINT_LABEL) != 0) { mHintLabel = null; } else { - mHintLabel = KeySpecParser.toUpperCaseOfStringForLocale(style.getString(keyAttr, + mHintLabel = StringUtils.toUpperCaseOfStringForLocale(style.getString(keyAttr, R.styleable.Keyboard_Key_keyHintLabel), needsToUpperCase, locale); } - String outputText = KeySpecParser.toUpperCaseOfStringForLocale(style.getString(keyAttr, - R.styleable.Keyboard_Key_keyOutputText), needsToUpperCase, locale); + String outputText = StringUtils.toUpperCaseOfStringForLocale( + KeySpecParser.getOutputText(keySpec), needsToUpperCase, locale); // Choose the first letter of the label as primary code if not specified. if (code == CODE_UNSPECIFIED && TextUtils.isEmpty(outputText) && !TextUtils.isEmpty(mLabel)) { if (StringUtils.codePointCount(mLabel) == 1) { // Use the first letter of the hint label if shiftedLetterActivated flag is // specified. - if (hasShiftedLetterHint() && isShiftedLetterActivated() - && !TextUtils.isEmpty(mHintLabel)) { + if (hasShiftedLetterHint() && isShiftedLetterActivated()) { mCode = mHintLabel.codePointAt(0); } else { mCode = mLabel.codePointAt(0); @@ -365,24 +354,20 @@ public class Key implements Comparable<Key> { mCode = CODE_OUTPUT_TEXT; } } else { - mCode = KeySpecParser.toUpperCaseOfCodeForLocale(code, needsToUpperCase, locale); + mCode = StringUtils.toUpperCaseOfCodeForLocale(code, needsToUpperCase, locale); } - final int altCode = KeySpecParser.toUpperCaseOfCodeForLocale( - KeySpecParser.parseCode(style.getString(keyAttr, - R.styleable.Keyboard_Key_altCode), params.mCodesSet, CODE_UNSPECIFIED), - needsToUpperCase, locale); + final int altCodeInAttr = KeySpecParser.parseCode( + style.getString(keyAttr, R.styleable.Keyboard_Key_altCode), CODE_UNSPECIFIED); + final int altCode = StringUtils.toUpperCaseOfCodeForLocale( + altCodeInAttr, needsToUpperCase, locale); mOptionalAttributes = OptionalAttributes.newInstance(outputText, altCode, disabledIconId, previewIconId, visualInsetsLeft, visualInsetsRight); mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr); - keyAttr.recycle(); mHashCode = computeHashCode(this); - if (hasShiftedLetterHint() && TextUtils.isEmpty(mHintLabel)) { - Log.w(TAG, "hasShiftedLetterHint specified without keyHintLabel: " + this); - } } /** - * Copy constructor. + * Copy constructor for DynamicGridKeyboard.GridKey. * * @param key the original key. */ @@ -486,15 +471,24 @@ public class Key implements Comparable<Key> { @Override public String toString() { - final String label; - if (StringUtils.codePointCount(mLabel) == 1 && mLabel.codePointAt(0) == mCode) { - label = ""; - } else { - label = "/" + mLabel; + return toShortString() + " " + getX() + "," + getY() + " " + getWidth() + "x" + getHeight(); + } + + public String toShortString() { + final int code = getCode(); + if (code == Constants.CODE_OUTPUT_TEXT) { + return getOutputText(); } - return String.format(Locale.ROOT, "%s%s %d,%d %dx%d %s/%s/%s", - Constants.printableCode(mCode), label, mX, mY, mWidth, mHeight, mHintLabel, - KeyboardIconsSet.getIconName(mIconId), backgroundName(mBackgroundType)); + return Constants.printableCode(code); + } + + public String toLongString() { + final int iconId = getIconId(); + final String topVisual = (iconId == KeyboardIconsSet.ICON_UNDEFINED) + ? KeyboardIconsSet.PREFIX_ICON + KeyboardIconsSet.getIconName(iconId) : getLabel(); + final String hintLabel = getHintLabel(); + final String visual = (hintLabel == null) ? topVisual : topVisual + "^" + hintLabel; + return toString() + " " + visual + "/" + backgroundName(mBackgroundType); } private static String backgroundName(final int backgroundType) { @@ -576,14 +570,16 @@ public class Key implements Comparable<Key> { } public final Typeface selectTypeface(final KeyDrawParams params) { - // TODO: Handle "bold" here too? - if ((mLabelFlags & LABEL_FLAGS_FONT_NORMAL) != 0) { + switch (mLabelFlags & LABEL_FLAGS_FONT_MASK) { + case LABEL_FLAGS_FONT_NORMAL: return Typeface.DEFAULT; - } - if ((mLabelFlags & LABEL_FLAGS_FONT_MONO_SPACE) != 0) { + case LABEL_FLAGS_FONT_MONO_SPACE: return Typeface.MONOSPACE; + case LABEL_FLAGS_FONT_DEFAULT: + default: + // The type-face is specified by keyTypeface attribute. + return params.mTypeface; } - return params.mTypeface; } public final int selectTextSize(final KeyDrawParams params) { @@ -604,22 +600,10 @@ public class Key implements Comparable<Key> { } public final int selectTextColor(final KeyDrawParams params) { - if (isShiftedLetterActivated()) { - return params.mTextInactivatedColor; - } - if (params.mTextColorStateList == null) { - return DEFAULT_TEXT_COLOR; + if ((mLabelFlags & LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR) != 0) { + return params.mFunctionalTextColor; } - final int[] state; - // TODO: Hack!!!!!!!! Consider having a new attribute for the functional text labels. - // Currently, we distinguish "input key" from "functional key" by checking the - // length of the label( > 1) and "functional" attributes (= true). - if (mLabel != null && mLabel.length() > 1) { - state = getCurrentDrawableState(); - } else { - state = KEY_STATE_NORMAL; - } - return params.mTextColorStateList.getColorForState(state, DEFAULT_TEXT_COLOR); + return isShiftedLetterActivated() ? params.mTextInactivatedColor : params.mTextColor; } public final int selectHintTextSize(final KeyDrawParams params) { @@ -678,6 +662,10 @@ public class Key implements Comparable<Key> { return (mLabelFlags & LABEL_FLAGS_ALIGN_RIGHT) != 0; } + public final boolean isAlignButtom() { + return (mLabelFlags & LABEL_FLAGS_ALIGN_BUTTOM) != 0; + } + public final boolean isAlignLeftOfCenter() { return (mLabelFlags & LABEL_FLAGS_ALIGN_LEFT_OF_CENTER) != 0; } @@ -687,7 +675,8 @@ public class Key implements Comparable<Key> { } public final boolean hasShiftedLetterHint() { - return (mLabelFlags & LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT) != 0; + return (mLabelFlags & LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT) != 0 + && !TextUtils.isEmpty(mHintLabel); } public final boolean hasHintLabel() { @@ -702,12 +691,17 @@ public class Key implements Comparable<Key> { return (mLabelFlags & LABEL_FLAGS_WITH_ICON_RIGHT) != 0; } - public final boolean needsXScale() { + public final boolean needsAutoXScale() { return (mLabelFlags & LABEL_FLAGS_AUTO_X_SCALE) != 0; } - public final boolean isShiftedLetterActivated() { - return (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) != 0; + public final boolean needsAutoScale() { + return (mLabelFlags & LABEL_FLAGS_AUTO_SCALE) == LABEL_FLAGS_AUTO_SCALE; + } + + private final boolean isShiftedLetterActivated() { + return (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) != 0 + && !TextUtils.isEmpty(mHintLabel); } public final int getMoreKeysColumn() { @@ -746,10 +740,14 @@ public class Key implements Comparable<Key> { return (attrs != null) ? attrs.mAltCode : CODE_UNSPECIFIED; } + public int getIconId() { + return mIconId; + } + public Drawable getIcon(final KeyboardIconsSet iconSet, final int alpha) { final OptionalAttributes attrs = mOptionalAttributes; final int disabledIconId = (attrs != null) ? attrs.mDisabledIconId : ICON_UNDEFINED; - final int iconId = mEnabled ? mIconId : disabledIconId; + final int iconId = mEnabled ? getIconId() : disabledIconId; final Drawable icon = iconSet.getIconDrawable(iconId); if (icon != null) { icon.setAlpha(alpha); @@ -761,7 +759,7 @@ public class Key implements Comparable<Key> { final OptionalAttributes attrs = mOptionalAttributes; final int previewIconId = (attrs != null) ? attrs.mPreviewIconId : ICON_UNDEFINED; return previewIconId != ICON_UNDEFINED - ? iconSet.getIconDrawable(previewIconId) : iconSet.getIconDrawable(mIconId); + ? iconSet.getIconDrawable(previewIconId) : iconSet.getIconDrawable(getIconId()); } public int getWidth() { @@ -883,17 +881,6 @@ public class Key implements Comparable<Key> { android.R.attr.state_empty }; - // functional normal state (with properties) - private static final int[] KEY_STATE_FUNCTIONAL_NORMAL = { - android.R.attr.state_single - }; - - // functional pressed state (with properties) - private static final int[] KEY_STATE_FUNCTIONAL_PRESSED = { - android.R.attr.state_single, - android.R.attr.state_pressed - }; - // action normal state (with properties) private static final int[] KEY_STATE_ACTIVE_NORMAL = { android.R.attr.state_active @@ -906,31 +893,49 @@ public class Key implements Comparable<Key> { }; /** - * Returns the drawable state for the key, based on the current state and type of the key. - * @return the drawable state of the key. + * Returns the background drawable for the key, based on the current state and type of the key. + * @return the background drawable of the key. * @see android.graphics.drawable.StateListDrawable#setState(int[]) */ - public final int[] getCurrentDrawableState() { + public final Drawable selectBackgroundDrawable(final Drawable keyBackground, + final Drawable functionalKeyBackground, final Drawable spacebarBackground) { + final Drawable background; + if (mBackgroundType == BACKGROUND_TYPE_FUNCTIONAL) { + background = functionalKeyBackground; + } else if (getCode() == Constants.CODE_SPACE) { + background = spacebarBackground; + } else { + background = keyBackground; + } + final int[] stateSet; switch (mBackgroundType) { - case BACKGROUND_TYPE_FUNCTIONAL: - return mPressed ? KEY_STATE_FUNCTIONAL_PRESSED : KEY_STATE_FUNCTIONAL_NORMAL; case BACKGROUND_TYPE_ACTION: - return mPressed ? KEY_STATE_ACTIVE_PRESSED : KEY_STATE_ACTIVE_NORMAL; + stateSet = mPressed ? KEY_STATE_ACTIVE_PRESSED : KEY_STATE_ACTIVE_NORMAL; + break; case BACKGROUND_TYPE_STICKY_OFF: - return mPressed ? KEY_STATE_PRESSED_HIGHLIGHT_OFF : KEY_STATE_NORMAL_HIGHLIGHT_OFF; + stateSet = mPressed ? KEY_STATE_PRESSED_HIGHLIGHT_OFF : KEY_STATE_NORMAL_HIGHLIGHT_OFF; + break; case BACKGROUND_TYPE_STICKY_ON: - return mPressed ? KEY_STATE_PRESSED_HIGHLIGHT_ON : KEY_STATE_NORMAL_HIGHLIGHT_ON; + stateSet = mPressed ? KEY_STATE_PRESSED_HIGHLIGHT_ON : KEY_STATE_NORMAL_HIGHLIGHT_ON; + break; case BACKGROUND_TYPE_EMPTY: - return mPressed ? KEY_STATE_PRESSED : KEY_STATE_EMPTY; + stateSet = mPressed ? KEY_STATE_PRESSED : KEY_STATE_EMPTY; + break; + case BACKGROUND_TYPE_FUNCTIONAL: + stateSet = mPressed ? KEY_STATE_PRESSED : KEY_STATE_NORMAL; + break; default: /* BACKGROUND_TYPE_NORMAL */ - return mPressed ? KEY_STATE_PRESSED : KEY_STATE_NORMAL; + stateSet = mPressed ? KEY_STATE_PRESSED : KEY_STATE_NORMAL; + break; } + background.setState(stateSet); + return background; } public static class Spacer extends Key { - public Spacer(final Resources res, final KeyboardParams params, final KeyboardRow row, - final XmlPullParser parser) throws XmlPullParserException { - super(res, params, row, parser); + public Spacer(final TypedArray keyAttr, final KeyStyle keyStyle, + final KeyboardParams params, final KeyboardRow row) { + super(null /* keySpec */, keyAttr, keyStyle, params, row); } /** @@ -938,8 +943,9 @@ public class Key implements Comparable<Key> { */ protected Spacer(final KeyboardParams params, final int x, final int y, final int width, final int height) { - super(params, null, null, ICON_UNDEFINED, CODE_UNSPECIFIED, - null, x, y, width, height, 0, BACKGROUND_TYPE_EMPTY); + super(null /* label */, ICON_UNDEFINED, CODE_UNSPECIFIED, null /* outputText */, + null /* hintLabel */, 0 /* labelFlags */, BACKGROUND_TYPE_EMPTY, x, y, width, + height, params.mHorizontalGap, params.mVerticalGap); } } } diff --git a/java/src/com/android/inputmethod/keyboard/KeyDetector.java b/java/src/com/android/inputmethod/keyboard/KeyDetector.java index befb6fa92..87368d4ef 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyDetector.java +++ b/java/src/com/android/inputmethod/keyboard/KeyDetector.java @@ -16,9 +16,9 @@ package com.android.inputmethod.keyboard; -import com.android.inputmethod.latin.Constants; - - +/** + * This class handles key detection. + */ public class KeyDetector { private final int mKeyHysteresisDistanceSquared; private final int mKeyHysteresisDistanceForSlidingModifierSquared; @@ -27,31 +27,27 @@ public class KeyDetector { private int mCorrectionX; private int mCorrectionY; - /** - * This class handles key detection. - * - * @param keyHysteresisDistance if the pointer movement distance is smaller than this, the - * movement will not be handled as meaningful movement. The unit is pixel. - */ - public KeyDetector(float keyHysteresisDistance) { - this(keyHysteresisDistance, keyHysteresisDistance); + public KeyDetector() { + this(0.0f /* keyHysteresisDistance */, 0.0f /* keyHysteresisDistanceForSlidingModifier */); } /** - * This class handles key detection. + * Key detection object constructor with key hysteresis distances. * * @param keyHysteresisDistance if the pointer movement distance is smaller than this, the * movement will not be handled as meaningful movement. The unit is pixel. * @param keyHysteresisDistanceForSlidingModifier the same parameter for sliding input that * starts from a modifier key such as shift and symbols key. */ - public KeyDetector(float keyHysteresisDistance, float keyHysteresisDistanceForSlidingModifier) { + public KeyDetector(final float keyHysteresisDistance, + final float keyHysteresisDistanceForSlidingModifier) { mKeyHysteresisDistanceSquared = (int)(keyHysteresisDistance * keyHysteresisDistance); mKeyHysteresisDistanceForSlidingModifierSquared = (int)( keyHysteresisDistanceForSlidingModifier * keyHysteresisDistanceForSlidingModifier); } - public void setKeyboard(Keyboard keyboard, float correctionX, float correctionY) { + public void setKeyboard(final Keyboard keyboard, final float correctionX, + final float correctionY) { if (keyboard == null) { throw new NullPointerException(); } @@ -60,28 +56,25 @@ public class KeyDetector { mKeyboard = keyboard; } - public int getKeyHysteresisDistanceSquared(boolean isSlidingFromModifier) { + public int getKeyHysteresisDistanceSquared(final boolean isSlidingFromModifier) { return isSlidingFromModifier ? mKeyHysteresisDistanceForSlidingModifierSquared : mKeyHysteresisDistanceSquared; } - public int getTouchX(int x) { + public int getTouchX(final int x) { return x + mCorrectionX; } // TODO: Remove vertical correction. - public int getTouchY(int y) { + public int getTouchY(final int y) { return y + mCorrectionY; } public Keyboard getKeyboard() { - if (mKeyboard == null) { - throw new IllegalStateException("keyboard isn't set"); - } return mKeyboard; } - public boolean alwaysAllowsSlidingInput() { + public boolean alwaysAllowsKeySelectionByDraggingFinger() { return false; } @@ -92,7 +85,10 @@ public class KeyDetector { * @param y The y-coordinate of a touch point * @return the key that the touch point hits. */ - public Key detectHitKey(int x, int y) { + public Key detectHitKey(final int x, final int y) { + if (mKeyboard == null) { + return null; + } final int touchX = getTouchX(x); final int touchY = getTouchY(y); @@ -117,20 +113,4 @@ public class KeyDetector { } return primaryKey; } - - public static String printableCode(Key key) { - return key != null ? Constants.printableCode(key.getCode()) : "none"; - } - - public static String printableCodes(int[] codes) { - final StringBuilder sb = new StringBuilder(); - boolean addDelimiter = false; - for (final int code : codes) { - if (code == Constants.NOT_A_CODE) break; - if (addDelimiter) sb.append(", "); - sb.append(Constants.printableCode(code)); - addDelimiter = true; - } - return "[" + sb + "]"; - } } diff --git a/java/src/com/android/inputmethod/keyboard/Keyboard.java b/java/src/com/android/inputmethod/keyboard/Keyboard.java index bc1383aff..85dfea4e7 100644 --- a/java/src/com/android/inputmethod/keyboard/Keyboard.java +++ b/java/src/com/android/inputmethod/keyboard/Keyboard.java @@ -22,7 +22,11 @@ import com.android.inputmethod.keyboard.internal.KeyVisualAttributes; import com.android.inputmethod.keyboard.internal.KeyboardIconsSet; import com.android.inputmethod.keyboard.internal.KeyboardParams; import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.latin.utils.CoordinateUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** * Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard @@ -73,13 +77,13 @@ public class Keyboard { /** Maximum column for more keys keyboard */ public final int mMaxMoreKeysKeyboardColumn; - /** Array of keys and icons in this keyboard */ - private final Key[] mKeys; - public final Key[] mShiftKeys; - public final Key[] mAltCodeKeysWhileTyping; + /** List of keys in this keyboard */ + private final List<Key> mSortedKeys; + public final List<Key> mShiftKeys; + public final List<Key> mAltCodeKeysWhileTyping; public final KeyboardIconsSet mIconsSet; - private final SparseArray<Key> mKeyCache = CollectionUtils.newSparseArray(); + private final SparseArray<Key> mKeyCache = new SparseArray<>(); private final ProximityInfo mProximityInfo; private final boolean mProximityCharsCorrectionEnabled; @@ -99,15 +103,15 @@ public class Keyboard { mTopPadding = params.mTopPadding; mVerticalGap = params.mVerticalGap; - mKeys = params.mKeys.toArray(new Key[params.mKeys.size()]); - mShiftKeys = params.mShiftKeys.toArray(new Key[params.mShiftKeys.size()]); - mAltCodeKeysWhileTyping = params.mAltCodeKeysWhileTyping.toArray( - new Key[params.mAltCodeKeysWhileTyping.size()]); + mSortedKeys = Collections.unmodifiableList(new ArrayList<>(params.mSortedKeys)); + mShiftKeys = Collections.unmodifiableList(params.mShiftKeys); + mAltCodeKeysWhileTyping = Collections.unmodifiableList(params.mAltCodeKeysWhileTyping); mIconsSet = params.mIconsSet; mProximityInfo = new ProximityInfo(params.mId.mLocale.toString(), params.GRID_WIDTH, params.GRID_HEIGHT, mOccupiedWidth, mOccupiedHeight, - mMostCommonKeyWidth, mMostCommonKeyHeight, mKeys, params.mTouchPositionCorrection); + mMostCommonKeyWidth, mMostCommonKeyHeight, mSortedKeys, + params.mTouchPositionCorrection); mProximityCharsCorrectionEnabled = params.mProximityCharsCorrectionEnabled; } @@ -126,7 +130,7 @@ public class Keyboard { mTopPadding = keyboard.mTopPadding; mVerticalGap = keyboard.mVerticalGap; - mKeys = keyboard.mKeys; + mSortedKeys = keyboard.mSortedKeys; mShiftKeys = keyboard.mShiftKeys; mAltCodeKeysWhileTyping = keyboard.mAltCodeKeysWhileTyping; mIconsSet = keyboard.mIconsSet; @@ -151,17 +155,14 @@ public class Keyboard { return mProximityInfo; } - public Key[] getKeys() { - return mKeys; - } - - public Key getKeyFromOutputText(final String outputText) { - for (final Key key : getKeys()) { - if (outputText.equals(key.getOutputText())) { - return key; - } - } - return null; + /** + * Return the sorted list of keys of this keyboard. + * The keys are sorted from top-left to bottom-right order. + * The list may contain {@link Key.Spacer} object as well. + * @return the sorted unmodifiable list of {@link Key}s of this keyboard. + */ + public List<Key> getSortedKeys() { + return mSortedKeys; } public Key getKey(final int code) { @@ -174,7 +175,7 @@ public class Keyboard { return mKeyCache.valueAt(index); } - for (final Key key : getKeys()) { + for (final Key key : getSortedKeys()) { if (key.getCode() == code) { mKeyCache.put(code, key); return key; @@ -190,7 +191,7 @@ public class Keyboard { return true; } - for (final Key key : getKeys()) { + for (final Key key : getSortedKeys()) { if (key == aKey) { mKeyCache.put(key.getCode(), key); return true; @@ -208,13 +209,29 @@ public class Keyboard { * Returns the array of the keys that are closest to the given point. * @param x the x-coordinate of the point * @param y the y-coordinate of the point - * @return the array of the nearest keys to the given point. If the given + * @return the list 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(final int x, final int y) { + public List<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 int[] getCoordinates(final int[] codePoints) { + final int length = codePoints.length; + final int[] coordinates = CoordinateUtils.newCoordinateArray(length); + for (int i = 0; i < length; ++i) { + final Key key = getKey(codePoints[i]); + if (null != key) { + CoordinateUtils.setXYInArray(coordinates, i, + key.getX() + key.getWidth() / 2, key.getY() + key.getHeight() / 2); + } else { + CoordinateUtils.setXYInArray(coordinates, i, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); + } + } + return coordinates; + } } diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java index dc760e685..c565866b7 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java @@ -53,8 +53,10 @@ public interface KeyboardActionListener { * {@link PointerTracker} or so, the value should be * {@link Constants#NOT_A_COORDINATE}.If it's called on insertion from the * suggestion strip, it should be {@link Constants#SUGGESTION_STRIP_COORDINATE}. + * @param isKeyRepeat true if this is a key repeat, false otherwise */ - public void onCodeInput(int primaryCode, int x, int y); + // TODO: change this to send an Event object instead + public void onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat); /** * Sends a string of characters to the listener. @@ -107,7 +109,7 @@ public interface KeyboardActionListener { @Override public void onReleaseKey(int primaryCode, boolean withSliding) {} @Override - public void onCodeInput(int primaryCode, int x, int y) {} + public void onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat) {} @Override public void onTextInput(String text) {} @Override diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardId.java b/java/src/com/android/inputmethod/keyboard/KeyboardId.java index 736f13ed6..3c1167538 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardId.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardId.java @@ -68,10 +68,8 @@ public final class KeyboardId { public final int mHeight; public final int mMode; public final int mElementId; - private final EditorInfo mEditorInfo; + public final EditorInfo mEditorInfo; public final boolean mClobberSettingsKey; - public final boolean mShortcutKeyEnabled; - public final boolean mShortcutKeyOnSymbols; public final boolean mLanguageSwitchKeyEnabled; public final String mCustomActionLabel; public final boolean mHasShortcutKey; @@ -87,17 +85,10 @@ public final class KeyboardId { mElementId = elementId; mEditorInfo = params.mEditorInfo; mClobberSettingsKey = params.mNoSettingsKey; - mShortcutKeyEnabled = params.mVoiceKeyEnabled; - mShortcutKeyOnSymbols = mShortcutKeyEnabled && !params.mVoiceKeyOnMain; mLanguageSwitchKeyEnabled = params.mLanguageSwitchKeyEnabled; mCustomActionLabel = (mEditorInfo.actionLabel != null) ? mEditorInfo.actionLabel.toString() : null; - final boolean alphabetMayHaveShortcutKey = isAlphabetKeyboard(elementId) - && !mShortcutKeyOnSymbols; - final boolean symbolsMayHaveShortcutKey = (elementId == KeyboardId.ELEMENT_SYMBOLS) - && mShortcutKeyOnSymbols; - mHasShortcutKey = mShortcutKeyEnabled - && (alphabetMayHaveShortcutKey || symbolsMayHaveShortcutKey); + mHasShortcutKey = params.mVoiceInputKeyEnabled; mHashCode = computeHashCode(this); } @@ -110,8 +101,7 @@ public final class KeyboardId { id.mHeight, id.passwordInput(), id.mClobberSettingsKey, - id.mShortcutKeyEnabled, - id.mShortcutKeyOnSymbols, + id.mHasShortcutKey, id.mLanguageSwitchKeyEnabled, id.isMultiLine(), id.imeAction(), @@ -131,8 +121,7 @@ public final class KeyboardId { && other.mHeight == mHeight && other.passwordInput() == passwordInput() && other.mClobberSettingsKey == mClobberSettingsKey - && other.mShortcutKeyEnabled == mShortcutKeyEnabled - && other.mShortcutKeyOnSymbols == mShortcutKeyOnSymbols + && other.mHasShortcutKey == mHasShortcutKey && other.mLanguageSwitchKeyEnabled == mLanguageSwitchKeyEnabled && other.isMultiLine() == isMultiLine() && other.imeAction() == imeAction() @@ -186,21 +175,19 @@ public final class KeyboardId { @Override public String toString() { - return String.format(Locale.ROOT, "[%s %s:%s %dx%d %s %s %s%s%s%s%s%s%s%s%s]", + return String.format(Locale.ROOT, "[%s %s:%s %dx%d %s %s%s%s%s%s%s%s%s]", elementIdToName(mElementId), mLocale, mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET), mWidth, mHeight, modeName(mMode), - imeAction(), - (navigateNext() ? "navigateNext" : ""), - (navigatePrevious() ? "navigatePrevious" : ""), + actionName(imeAction()), + (navigateNext() ? " navigateNext" : ""), + (navigatePrevious() ? " navigatePrevious" : ""), (mClobberSettingsKey ? " clobberSettingsKey" : ""), (passwordInput() ? " passwordInput" : ""), - (mShortcutKeyEnabled ? " shortcutKeyEnabled" : ""), - (mShortcutKeyOnSymbols ? " shortcutKeyOnSymbols" : ""), (mHasShortcutKey ? " hasShortcutKey" : ""), (mLanguageSwitchKeyEnabled ? " languageSwitchKeyEnabled" : ""), - (isMultiLine() ? "isMultiLine" : "") + (isMultiLine() ? " isMultiLine" : "") ); } diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java index 1eccdf341..d01c4cc8f 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java @@ -17,10 +17,7 @@ package com.android.inputmethod.keyboard; import static com.android.inputmethod.latin.Constants.ImeOption.FORCE_ASCII; -import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE; -import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE_COMPAT; import static com.android.inputmethod.latin.Constants.ImeOption.NO_SETTINGS_KEY; -import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.ASCII_CAPABLE; import android.content.Context; import android.content.res.Resources; @@ -34,6 +31,7 @@ import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.compat.EditorInfoCompatUtils; +import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils; import com.android.inputmethod.keyboard.internal.KeyboardBuilder; import com.android.inputmethod.keyboard.internal.KeyboardParams; import com.android.inputmethod.keyboard.internal.KeysCache; @@ -41,8 +39,8 @@ import com.android.inputmethod.latin.InputAttributes; import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.SubtypeSwitcher; -import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.InputTypeUtils; +import com.android.inputmethod.latin.utils.ScriptUtils; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; import com.android.inputmethod.latin.utils.XmlParseUtils; @@ -66,6 +64,7 @@ public final class KeyboardLayoutSet { private static final String TAG_KEYBOARD_SET = "KeyboardLayoutSet"; private static final String TAG_ELEMENT = "Element"; + private static final String TAG_FEATURE = "Feature"; private static final String KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX = "keyboard_layout_set_"; @@ -81,7 +80,7 @@ public final class KeyboardLayoutSet { // them from disappearing from sKeyboardCache. private static final Keyboard[] sForcibleKeyboardCache = new Keyboard[FORCIBLE_CACHE_SIZE]; private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache = - CollectionUtils.newHashMap(); + new HashMap<>(); private static final KeysCache sKeysCache = new KeysCache(); @SuppressWarnings("serial") @@ -103,21 +102,21 @@ public final class KeyboardLayoutSet { public static final class Params { String mKeyboardLayoutSetName; int mMode; - EditorInfo mEditorInfo; boolean mDisableTouchPositionCorrectionDataForTest; - boolean mVoiceKeyEnabled; - // TODO: Remove mVoiceKeyOnMain when it's certainly confirmed that we no longer show - // the voice input key on the symbol layout - boolean mVoiceKeyOnMain; + // TODO: Use {@link InputAttributes} instead of these variables. + EditorInfo mEditorInfo; + boolean mIsPasswordField; + boolean mVoiceInputKeyEnabled; boolean mNoSettingsKey; boolean mLanguageSwitchKeyEnabled; InputMethodSubtype mSubtype; boolean mIsSpellChecker; int mKeyboardWidth; int mKeyboardHeight; + int mScriptId; // Sparse array of KeyboardLayoutSet element parameters indexed by element's id. final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap = - CollectionUtils.newSparseArray(); + new SparseArray<>(); } public static void clearKeyboardCache() { @@ -181,7 +180,7 @@ public final class KeyboardLayoutSet { } final KeyboardBuilder<KeyboardParams> builder = - new KeyboardBuilder<KeyboardParams>(mContext, new KeyboardParams()); + new KeyboardBuilder<>(mContext, new KeyboardParams()); if (id.isAlphabetKeyboard()) { builder.setAutoGenerate(sKeysCache); } @@ -192,7 +191,7 @@ public final class KeyboardLayoutSet { } builder.setProximityCharsCorrectionEnabled(elementParams.mProximityCharsCorrectionEnabled); final Keyboard keyboard = builder.build(); - sKeyboardCache.put(id, new SoftReference<Keyboard>(keyboard)); + sKeyboardCache.put(id, new SoftReference<>(keyboard)); if ((id.mElementId == KeyboardId.ELEMENT_ALPHABET || id.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) && !mParams.mIsSpellChecker) { @@ -212,6 +211,10 @@ public final class KeyboardLayoutSet { return keyboard; } + public int getScriptId() { + return mParams.mScriptId; + } + public static final class Builder { private final Context mContext; private final String mPackageName; @@ -221,16 +224,19 @@ public final class KeyboardLayoutSet { private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo(); - public Builder(final Context context, final EditorInfo editorInfo) { + public Builder(final Context context, final EditorInfo ei) { mContext = context; mPackageName = context.getPackageName(); mResources = context.getResources(); final Params params = mParams; + final EditorInfo editorInfo = (ei != null) ? ei : EMPTY_EDITOR_INFO; params.mMode = getKeyboardMode(editorInfo); - params.mEditorInfo = (editorInfo != null) ? editorInfo : EMPTY_EDITOR_INFO; + // TODO: Consolidate those with {@link InputAttributes}. + params.mEditorInfo = editorInfo; + params.mIsPasswordField = InputTypeUtils.isPasswordInputType(editorInfo.inputType); params.mNoSettingsKey = InputAttributes.inPrivateImeOptions( - mPackageName, NO_SETTINGS_KEY, params.mEditorInfo); + mPackageName, NO_SETTINGS_KEY, editorInfo); } public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) { @@ -240,7 +246,8 @@ public final class KeyboardLayoutSet { } public Builder setSubtype(final InputMethodSubtype subtype) { - final boolean asciiCapable = subtype.containsExtraValueKey(ASCII_CAPABLE); + final boolean asciiCapable = InputMethodSubtypeCompatUtils.isAsciiCapable(subtype); + // TODO: Consolidate with {@link InputAttributes}. @SuppressWarnings("deprecation") final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions( mPackageName, FORCE_ASCII, mParams.mEditorInfo); @@ -261,19 +268,13 @@ public final class KeyboardLayoutSet { return this; } - // TODO: Remove mVoiceKeyOnMain when it's certainly confirmed that we no longer show - // the voice input key on the symbol layout - public Builder setOptions(final boolean voiceKeyEnabled, final boolean voiceKeyOnMain, - final boolean languageSwitchKeyEnabled) { - @SuppressWarnings("deprecation") - final boolean deprecatedNoMicrophone = InputAttributes.inPrivateImeOptions( - null, NO_MICROPHONE_COMPAT, mParams.mEditorInfo); - final boolean noMicrophone = InputAttributes.inPrivateImeOptions( - mPackageName, NO_MICROPHONE, mParams.mEditorInfo) - || deprecatedNoMicrophone; - mParams.mVoiceKeyEnabled = voiceKeyEnabled && !noMicrophone; - mParams.mVoiceKeyOnMain = voiceKeyOnMain; - mParams.mLanguageSwitchKeyEnabled = languageSwitchKeyEnabled; + public Builder setVoiceInputKeyEnabled(final boolean enabled) { + mParams.mVoiceInputKeyEnabled = enabled; + return this; + } + + public Builder setLanguageSwitchKeyEnabled(final boolean enabled) { + mParams.mLanguageSwitchKeyEnabled = enabled; return this; } @@ -281,6 +282,10 @@ public final class KeyboardLayoutSet { mParams.mDisableTouchPositionCorrectionDataForTest = true; } + public void setScriptId(final int scriptId) { + mParams.mScriptId = scriptId; + } + public KeyboardLayoutSet build() { if (mParams.mSubtype == null) throw new RuntimeException("KeyboardLayoutSet subtype is not specified"); @@ -326,6 +331,8 @@ public final class KeyboardLayoutSet { final String tag = parser.getName(); if (TAG_ELEMENT.equals(tag)) { parseKeyboardLayoutSetElement(parser); + } else if (TAG_FEATURE.equals(tag)) { + parseKeyboardLayoutSetFeature(parser); } else { throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET); } @@ -367,10 +374,22 @@ public final class KeyboardLayoutSet { } } - private static int getKeyboardMode(final EditorInfo editorInfo) { - if (editorInfo == null) - return KeyboardId.MODE_TEXT; + private void parseKeyboardLayoutSetFeature(final XmlPullParser parser) + throws XmlPullParserException, IOException { + final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), + R.styleable.KeyboardLayoutSet_Feature); + try { + final int scriptId = a.getInt( + R.styleable.KeyboardLayoutSet_Feature_supportedScript, + ScriptUtils.SCRIPT_LATIN); + XmlParseUtils.checkEndTag(TAG_FEATURE, parser); + setScriptId(scriptId); + } finally { + a.recycle(); + } + } + private static int getKeyboardMode(final EditorInfo editorInfo) { final int inputType = editorInfo.inputType; final int variation = inputType & InputType.TYPE_MASK_VARIATION; diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java index 5abc9ab38..f35126750 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java @@ -26,46 +26,24 @@ import android.view.LayoutInflater; import android.view.View; import android.view.inputmethod.EditorInfo; -import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy; import com.android.inputmethod.compat.InputMethodServiceCompatUtils; import com.android.inputmethod.keyboard.KeyboardLayoutSet.KeyboardLayoutSetException; +import com.android.inputmethod.keyboard.emoji.EmojiPalettesView; import com.android.inputmethod.keyboard.internal.KeyboardState; +import com.android.inputmethod.keyboard.internal.KeyboardTextsSet; import com.android.inputmethod.latin.InputView; import com.android.inputmethod.latin.LatinIME; -import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.RichInputMethodManager; import com.android.inputmethod.latin.SubtypeSwitcher; import com.android.inputmethod.latin.WordComposer; -import com.android.inputmethod.latin.settings.Settings; import com.android.inputmethod.latin.settings.SettingsValues; import com.android.inputmethod.latin.utils.ResourceUtils; +import com.android.inputmethod.latin.utils.ScriptUtils; public final class KeyboardSwitcher implements KeyboardState.SwitchActions { private static final String TAG = KeyboardSwitcher.class.getSimpleName(); - static final class KeyboardTheme { - public final int mThemeId; - public final int mStyleId; - - // Note: The themeId should be aligned with "themeId" attribute of Keyboard style - // in values/style.xml. - public KeyboardTheme(final int themeId, final int styleId) { - mThemeId = themeId; - mStyleId = styleId; - } - } - - public static final int THEME_INDEX_ICS = 0; - public static final int THEME_INDEX_GB = 1; - public static final int THEME_INDEX_KLP = 2; - public static final int THEME_INDEX_DEFAULT = THEME_INDEX_KLP; - public static final KeyboardTheme[] KEYBOARD_THEMES = { - new KeyboardTheme(THEME_INDEX_ICS, R.style.KeyboardTheme_ICS), - new KeyboardTheme(THEME_INDEX_GB, R.style.KeyboardTheme_GB), - new KeyboardTheme(THEME_INDEX_KLP, R.style.KeyboardTheme_KLP), - }; - private SubtypeSwitcher mSubtypeSwitcher; private SharedPreferences mPrefs; @@ -74,18 +52,16 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { private MainKeyboardView mKeyboardView; private EmojiPalettesView mEmojiPalettesView; private LatinIME mLatinIME; - private Resources mResources; private boolean mIsHardwareAcceleratedDrawingEnabled; private KeyboardState mState; private KeyboardLayoutSet mKeyboardLayoutSet; + // TODO: The following {@link KeyboardTextsSet} should be in {@link KeyboardLayoutSet}. + private final KeyboardTextsSet mKeyboardTextsSet = new KeyboardTextsSet(); + private SettingsValues mCurrentSettingsValues; - /** mIsAutoCorrectionActive indicates that auto corrected word will be input instead of - * what user actually typed. */ - private boolean mIsAutoCorrectionActive; - - private KeyboardTheme mKeyboardTheme = KEYBOARD_THEMES[THEME_INDEX_DEFAULT]; + private KeyboardTheme mKeyboardTheme; private Context mThemeContext; private static final KeyboardSwitcher sInstance = new KeyboardSwitcher(); @@ -105,7 +81,6 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { private void initInternal(final LatinIME latinIme, final SharedPreferences prefs) { mLatinIME = latinIme; - mResources = latinIme.getResources(); mPrefs = prefs; mSubtypeSwitcher = SubtypeSwitcher.getInstance(); mState = new KeyboardState(this); @@ -115,28 +90,15 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { public void updateKeyboardTheme() { final boolean themeUpdated = updateKeyboardThemeAndContextThemeWrapper( - mLatinIME, getKeyboardTheme(mLatinIME, mPrefs)); + mLatinIME, KeyboardTheme.getKeyboardTheme(mPrefs)); if (themeUpdated && mKeyboardView != null) { mLatinIME.setInputView(onCreateInputView(mIsHardwareAcceleratedDrawingEnabled)); } } - private static KeyboardTheme getKeyboardTheme(final Context context, - final SharedPreferences prefs) { - final Resources res = context.getResources(); - final int index = Settings.readKeyboardThemeIndex(prefs, res); - if (index >= 0 && index < KEYBOARD_THEMES.length) { - return KEYBOARD_THEMES[index]; - } - final int defaultThemeIndex = Settings.resetAndGetDefaultKeyboardThemeIndex(prefs, res); - Log.w(TAG, "Illegal keyboard theme in preference: " + index + ", default to " - + defaultThemeIndex); - return KEYBOARD_THEMES[defaultThemeIndex]; - } - private boolean updateKeyboardThemeAndContextThemeWrapper(final Context context, final KeyboardTheme keyboardTheme) { - if (mThemeContext == null || mKeyboardTheme.mThemeId != keyboardTheme.mThemeId) { + if (mThemeContext == null || !keyboardTheme.equals(mKeyboardTheme)) { mKeyboardTheme = keyboardTheme; mThemeContext = new ContextThemeWrapper(context, keyboardTheme.mStyleId); KeyboardLayoutSet.clearKeyboardCache(); @@ -145,7 +107,8 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { return false; } - public void loadKeyboard(final EditorInfo editorInfo, final SettingsValues settingsValues) { + public void loadKeyboard(final EditorInfo editorInfo, final SettingsValues settingsValues, + final int currentAutoCapsState, final int currentRecapitalizeState) { final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder( mThemeContext, editorInfo); final Resources res = mThemeContext.getResources(); @@ -153,16 +116,15 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { final int keyboardHeight = ResourceUtils.getDefaultKeyboardHeight(res); builder.setKeyboardGeometry(keyboardWidth, keyboardHeight); builder.setSubtype(mSubtypeSwitcher.getCurrentSubtype()); - builder.setOptions( - settingsValues.isVoiceKeyEnabled(editorInfo), - true /* always show a voice key on the main keyboard */, - settingsValues.isLanguageSwitchKeyEnabled()); + builder.setVoiceInputKeyEnabled(settingsValues.mShowsVoiceInputKey); + builder.setLanguageSwitchKeyEnabled(mLatinIME.shouldShowLanguageSwitchKey()); mKeyboardLayoutSet = builder.build(); + mCurrentSettingsValues = settingsValues; try { - mState.onLoadKeyboard(); + mState.onLoadKeyboard(currentAutoCapsState, currentRecapitalizeState); + mKeyboardTextsSet.setLocale(mSubtypeSwitcher.getCurrentSubtypeLocale(), mThemeContext); } catch (KeyboardLayoutSetException e) { Log.w(TAG, "loading keyboard failed: " + e.mKeyboardId, e.getCause()); - LatinImeLogger.logOnException(e.mKeyboardId.toString(), e.getCause()); return; } } @@ -173,12 +135,10 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { } } - public void onFinishInputView() { - mIsAutoCorrectionActive = false; - } - public void onHideWindow() { - mIsAutoCorrectionActive = false; + if (mKeyboardView != null) { + mKeyboardView.onHideWindow(); + } } private void setKeyboard(final Keyboard keyboard) { @@ -187,18 +147,24 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { final MainKeyboardView keyboardView = mKeyboardView; final Keyboard oldKeyboard = keyboardView.getKeyboard(); keyboardView.setKeyboard(keyboard); - mCurrentInputView.setKeyboardGeometry(keyboard.mTopPadding); + mCurrentInputView.setKeyboardTopPadding(keyboard.mTopPadding); keyboardView.setKeyPreviewPopupEnabled( - Settings.readKeyPreviewPopupEnabled(mPrefs, mResources), - Settings.readKeyPreviewPopupDismissDelay(mPrefs, mResources)); - keyboardView.updateAutoCorrectionState(mIsAutoCorrectionActive); + mCurrentSettingsValues.mKeyPreviewPopupOn, + mCurrentSettingsValues.mKeyPreviewPopupDismissDelay); + keyboardView.setKeyPreviewAnimationParams( + mCurrentSettingsValues.mKeyPreviewShowUpStartScale, + mCurrentSettingsValues.mKeyPreviewShowUpDuration, + mCurrentSettingsValues.mKeyPreviewDismissEndScale, + mCurrentSettingsValues.mKeyPreviewDismissDuration); keyboardView.updateShortcutKey(mSubtypeSwitcher.isShortcutImeReady()); final boolean subtypeChanged = (oldKeyboard == null) || !keyboard.mId.mLocale.equals(oldKeyboard.mId.mLocale); - final boolean needsToDisplayLanguage = mSubtypeSwitcher.needsToDisplayLanguage( - keyboard.mId.mLocale); - keyboardView.startDisplayLanguageOnSpacebar(subtypeChanged, needsToDisplayLanguage, - RichInputMethodManager.getInstance().hasMultipleEnabledIMEsOrSubtypes(true)); + final int languageOnSpacebarFormatType = mSubtypeSwitcher.getLanguageOnSpacebarFormatType( + keyboard.mId.mSubtype); + final boolean hasMultipleEnabledIMEsOrSubtypes = RichInputMethodManager.getInstance() + .hasMultipleEnabledIMEsOrSubtypes(true /* shouldIncludeAuxiliarySubtypes */); + keyboardView.startDisplayLanguageOnSpacebar(subtypeChanged, languageOnSpacebarFormatType, + hasMultipleEnabledIMEsOrSubtypes); } public Keyboard getKeyboard() { @@ -208,30 +174,26 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { return null; } - /** - * Update keyboard shift state triggered by connected EditText status change. - */ - public void updateShiftState() { - mState.onUpdateShiftState(mLatinIME.getCurrentAutoCapsState(), - mLatinIME.getCurrentRecapitalizeState()); - } - // TODO: Remove this method. Come up with a more comprehensive way to reset the keyboard layout // when a keyboard layout set doesn't get reloaded in LatinIME.onStartInputViewInternal(). - public void resetKeyboardStateToAlphabet() { - mState.onResetKeyboardStateToAlphabet(); + public void resetKeyboardStateToAlphabet(final int currentAutoCapsState, + final int currentRecapitalizeState) { + mState.onResetKeyboardStateToAlphabet(currentAutoCapsState, currentRecapitalizeState); } - public void onPressKey(final int code, final boolean isSinglePointer) { - mState.onPressKey(code, isSinglePointer, mLatinIME.getCurrentAutoCapsState()); + public void onPressKey(final int code, final boolean isSinglePointer, + final int currentAutoCapsState, final int currentRecapitalizeState) { + mState.onPressKey(code, isSinglePointer, currentAutoCapsState, currentRecapitalizeState); } - public void onReleaseKey(final int code, final boolean withSliding) { - mState.onReleaseKey(code, withSliding); + public void onReleaseKey(final int code, final boolean withSliding, + final int currentAutoCapsState, final int currentRecapitalizeState) { + mState.onReleaseKey(code, withSliding, currentAutoCapsState, currentRecapitalizeState); } - public void onFinishSlidingInput() { - mState.onFinishSlidingInput(); + public void onFinishSlidingInput(final int currentAutoCapsState, + final int currentRecapitalizeState) { + mState.onFinishSlidingInput(currentAutoCapsState, currentRecapitalizeState); } // Implements {@link KeyboardState.SwitchActions}. @@ -279,8 +241,11 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { // Implements {@link KeyboardState.SwitchActions}. @Override public void setEmojiKeyboard() { + final Keyboard keyboard = mKeyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET); mMainKeyboardFrame.setVisibility(View.GONE); - mEmojiPalettesView.startEmojiPalettes(); + mEmojiPalettesView.startEmojiPalettes( + mKeyboardTextsSet.getText(KeyboardTextsSet.SWITCH_TO_ALPHA_KEY_LABEL), + mKeyboardView.getKeyVisualAttribute(), keyboard.mIconsSet); mEmojiPalettesView.setVisibility(View.VISIBLE); } @@ -290,11 +255,10 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { setKeyboard(mKeyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_SYMBOLS_SHIFTED)); } - // Implements {@link KeyboardState.SwitchActions}. - @Override - public void requestUpdatingShiftState() { - mState.onUpdateShiftState(mLatinIME.getCurrentAutoCapsState(), - mLatinIME.getCurrentRecapitalizeState()); + // Future method for requesting an updating to the shift state. + public void requestUpdatingShiftState(final int currentAutoCapsState, + final int currentRecapitalizeState) { + mState.onUpdateShiftState(currentAutoCapsState, currentRecapitalizeState); } // Implements {@link KeyboardState.SwitchActions}. @@ -325,12 +289,9 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { /** * Updates state machine to figure out when to automatically switch back to the previous mode. */ - public void onCodeInput(final int code) { - mState.onCodeInput(code, mLatinIME.getCurrentAutoCapsState()); - } - - private boolean isShowingMainKeyboard() { - return null != mKeyboardView && mKeyboardView.isShown(); + public void onCodeInput(final int code, final int currentAutoCapsState, + final int currentRecapitalizeState) { + mState.onCodeInput(code, currentAutoCapsState, currentRecapitalizeState); } public boolean isShowingEmojiPalettes() { @@ -365,16 +326,13 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { } } - public boolean isShowingMainKeyboardOrEmojiPalettes() { - return isShowingMainKeyboard() || isShowingEmojiPalettes(); - } - public View onCreateInputView(final boolean isHardwareAcceleratedDrawingEnabled) { if (mKeyboardView != null) { mKeyboardView.closing(); } - updateKeyboardThemeAndContextThemeWrapper(mLatinIME, mKeyboardTheme); + updateKeyboardThemeAndContextThemeWrapper( + mLatinIME, KeyboardTheme.getKeyboardTheme(mPrefs)); mCurrentInputView = (InputView)LayoutInflater.from(mThemeContext).inflate( R.layout.input_view, null); mMainKeyboardFrame = mCurrentInputView.findViewById(R.id.main_keyboard_frame); @@ -387,11 +345,6 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { mEmojiPalettesView.setHardwareAcceleratedDrawingEnabled( isHardwareAcceleratedDrawingEnabled); mEmojiPalettesView.setKeyboardActionListener(mLatinIME); - - // This always needs to be set since the accessibility state can - // potentially change without the input view being re-created. - AccessibleKeyboardViewProxy.getInstance().setView(mKeyboardView); - return mCurrentInputView; } @@ -401,15 +354,6 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { } } - public void onAutoCorrectionStateChanged(final boolean isAutoCorrection) { - if (mIsAutoCorrectionActive != isAutoCorrection) { - mIsAutoCorrectionActive = isAutoCorrection; - if (mKeyboardView != null) { - mKeyboardView.updateAutoCorrectionState(isAutoCorrection); - } - } - } - public int getKeyboardShiftMode() { final Keyboard keyboard = getKeyboard(); if (keyboard == null) { @@ -427,4 +371,11 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { return WordComposer.CAPS_MODE_OFF; } } + + public int getCurrentKeyboardScriptId() { + if (null == mKeyboardLayoutSet) { + return ScriptUtils.SCRIPT_UNKNOWN; + } + return mKeyboardLayoutSet.getScriptId(); + } } diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardTheme.java b/java/src/com/android/inputmethod/keyboard/KeyboardTheme.java new file mode 100644 index 000000000..7b41dfef6 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/KeyboardTheme.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard; + +import android.content.SharedPreferences; +import android.os.Build; +import android.os.Build.VERSION_CODES; +import android.util.Log; + +import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.R; + +import java.util.Arrays; + +public final class KeyboardTheme implements Comparable<KeyboardTheme> { + private static final String TAG = KeyboardTheme.class.getSimpleName(); + + static final String KLP_KEYBOARD_THEME_KEY = "pref_keyboard_layout_20110916"; + static final String LXX_KEYBOARD_THEME_KEY = "pref_keyboard_theme_20140509"; + + public static final int THEME_ID_ICS = 0; + public static final int THEME_ID_KLP = 2; + public static final int THEME_ID_LXX_DARK = 3; + public static final int DEFAULT_THEME_ID = THEME_ID_KLP; + + private static final KeyboardTheme[] KEYBOARD_THEMES = { + new KeyboardTheme(THEME_ID_ICS, R.style.KeyboardTheme_ICS, + // This has never been selected because we support ICS or later. + VERSION_CODES.BASE), + new KeyboardTheme(THEME_ID_KLP, R.style.KeyboardTheme_KLP, + // Default theme for ICS, JB, and KLP. + VERSION_CODES.ICE_CREAM_SANDWICH), + new KeyboardTheme(THEME_ID_LXX_DARK, R.style.KeyboardTheme_LXX_Dark, + // Default theme for LXX. + // TODO: Update this constant once the *next* version becomes available. + VERSION_CODES.CUR_DEVELOPMENT), + }; + + static { + // Sort {@link #KEYBOARD_THEME} by descending order of {@link #mMinApiVersion}. + Arrays.sort(KEYBOARD_THEMES); + } + + public final int mThemeId; + public final int mStyleId; + private final int mMinApiVersion; + + // Note: The themeId should be aligned with "themeId" attribute of Keyboard style + // in values/themes-<style>.xml. + private KeyboardTheme(final int themeId, final int styleId, final int minApiVersion) { + mThemeId = themeId; + mStyleId = styleId; + mMinApiVersion = minApiVersion; + } + + @Override + public int compareTo(final KeyboardTheme rhs) { + if (mMinApiVersion > rhs.mMinApiVersion) return -1; + if (mMinApiVersion < rhs.mMinApiVersion) return 1; + return 0; + } + + @Override + public boolean equals(final Object o) { + if (o == this) return true; + return (o instanceof KeyboardTheme) && ((KeyboardTheme)o).mThemeId == mThemeId; + } + + @Override + public int hashCode() { + return mThemeId; + } + + @UsedForTesting + static KeyboardTheme searchKeyboardThemeById(final int themeId) { + // TODO: This search algorithm isn't optimal if there are many themes. + for (final KeyboardTheme theme : KEYBOARD_THEMES) { + if (theme.mThemeId == themeId) { + return theme; + } + } + return null; + } + + private static int getSdkVersion() { + final int sdkVersion = Build.VERSION.SDK_INT; + // TODO: Consider to remove this check once the *next* version becomes available. + if (sdkVersion > VERSION_CODES.KITKAT) { + return VERSION_CODES.CUR_DEVELOPMENT; + } + return sdkVersion; + } + + @UsedForTesting + static KeyboardTheme getDefaultKeyboardTheme(final SharedPreferences prefs, + final int sdkVersion) { + final String klpThemeIdString = prefs.getString(KLP_KEYBOARD_THEME_KEY, null); + if (klpThemeIdString != null) { + if (sdkVersion <= VERSION_CODES.KITKAT) { + try { + final int themeId = Integer.parseInt(klpThemeIdString); + final KeyboardTheme theme = searchKeyboardThemeById(themeId); + if (theme != null) { + return theme; + } + Log.w(TAG, "Unknown keyboard theme in KLP preference: " + klpThemeIdString); + } catch (final NumberFormatException e) { + Log.w(TAG, "Illegal keyboard theme in KLP preference: " + klpThemeIdString, e); + } + } + // Remove old preference. + Log.i(TAG, "Remove KLP keyboard theme preference: " + klpThemeIdString); + prefs.edit().remove(KLP_KEYBOARD_THEME_KEY).apply(); + } + // TODO: This search algorithm isn't optimal if there are many themes. + for (final KeyboardTheme theme : KEYBOARD_THEMES) { + if (sdkVersion >= theme.mMinApiVersion) { + return theme; + } + } + return searchKeyboardThemeById(DEFAULT_THEME_ID); + } + + public static void saveKeyboardThemeId(final String themeIdString, + final SharedPreferences prefs) { + saveKeyboardThemeId(themeIdString, prefs, getSdkVersion()); + } + + @UsedForTesting + static String getPreferenceKey(final int sdkVersion) { + if (sdkVersion <= VERSION_CODES.KITKAT) { + return KLP_KEYBOARD_THEME_KEY; + } + return LXX_KEYBOARD_THEME_KEY; + } + + @UsedForTesting + static void saveKeyboardThemeId(final String themeIdString, + final SharedPreferences prefs, final int sdkVersion) { + final String prefKey = getPreferenceKey(sdkVersion); + prefs.edit().putString(prefKey, themeIdString).apply(); + } + + public static KeyboardTheme getKeyboardTheme(final SharedPreferences prefs) { + return getKeyboardTheme(prefs, getSdkVersion()); + } + + @UsedForTesting + static KeyboardTheme getKeyboardTheme(final SharedPreferences prefs, final int sdkVersion) { + final String lxxThemeIdString = prefs.getString(LXX_KEYBOARD_THEME_KEY, null); + if (lxxThemeIdString == null) { + return getDefaultKeyboardTheme(prefs, sdkVersion); + } + try { + final int themeId = Integer.parseInt(lxxThemeIdString); + final KeyboardTheme theme = searchKeyboardThemeById(themeId); + if (theme != null) { + return theme; + } + Log.w(TAG, "Unknown keyboard theme in LXX preference: " + lxxThemeIdString); + } catch (final NumberFormatException e) { + Log.w(TAG, "Illegal keyboard theme in LXX preference: " + lxxThemeIdString, e); + } + // Remove preference that contains unknown or illegal theme id. + prefs.edit().remove(LXX_KEYBOARD_THEME_KEY).apply(); + return getDefaultKeyboardTheme(prefs, sdkVersion); + } +} diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java index 5578713a0..c4ca1c495 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java @@ -28,18 +28,15 @@ import android.graphics.Rect; import android.graphics.Region; import android.graphics.Typeface; import android.graphics.drawable.Drawable; +import android.graphics.drawable.NinePatchDrawable; import android.util.AttributeSet; import android.view.View; import com.android.inputmethod.keyboard.internal.KeyDrawParams; import com.android.inputmethod.keyboard.internal.KeyVisualAttributes; import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.define.ProductionFlag; -import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.TypefaceUtils; -import com.android.inputmethod.research.ResearchLogger; import java.util.HashSet; @@ -47,6 +44,9 @@ import java.util.HashSet; * A view that renders a virtual {@link Keyboard}. * * @attr ref R.styleable#KeyboardView_keyBackground + * @attr ref R.styleable#KeyboardView_functionalKeyBackground + * @attr ref R.styleable#KeyboardView_spacebarBackground + * @attr ref R.styleable#KeyboardView_spacebarIconWidthRatio * @attr ref R.styleable#KeyboardView_keyLabelHorizontalPadding * @attr ref R.styleable#KeyboardView_keyHintLetterPadding * @attr ref R.styleable#KeyboardView_keyPopupHintLetterPadding @@ -81,7 +81,11 @@ public class KeyboardView extends View { private final float mKeyTextShadowRadius; private final float mVerticalCorrection; private final Drawable mKeyBackground; + private final Drawable mFunctionalKeyBackground; + private final Drawable mSpacebarBackground; + private final float mSpacebarIconWidthRatio; private final Rect mKeyBackgroundPadding = new Rect(); + private static final float KET_TEXT_SHADOW_RADIUS_DISABLED = -1.0f; // HORIZONTAL ELLIPSIS "...", character for popup hint. private static final String POPUP_HINT_CHAR = "\u2026"; @@ -102,7 +106,7 @@ public class KeyboardView extends View { /** True if all keys should be drawn */ private boolean mInvalidateAllKeys; /** The keys that should be drawn */ - private final HashSet<Key> mInvalidatedKeys = CollectionUtils.newHashSet(); + private final HashSet<Key> mInvalidatedKeys = new HashSet<>(); /** The working rectangle variable */ private final Rect mWorkingRect = new Rect(); /** The keyboard bitmap buffer for faster updates */ @@ -113,9 +117,6 @@ public class KeyboardView extends View { private final Canvas mOffscreenCanvas = new Canvas(); private final Paint mPaint = new Paint(); private final Paint.FontMetrics mFontMetrics = new Paint.FontMetrics(); - private static final char[] KEY_LABEL_REFERENCE_CHAR = { 'M' }; - private static final char[] KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR = { '8' }; - public KeyboardView(final Context context, final AttributeSet attrs) { this(context, attrs, R.attr.keyboardViewStyle); } @@ -127,6 +128,15 @@ public class KeyboardView extends View { R.styleable.KeyboardView, defStyle, R.style.KeyboardView); mKeyBackground = keyboardViewAttr.getDrawable(R.styleable.KeyboardView_keyBackground); mKeyBackground.getPadding(mKeyBackgroundPadding); + final Drawable functionalKeyBackground = keyboardViewAttr.getDrawable( + R.styleable.KeyboardView_functionalKeyBackground); + mFunctionalKeyBackground = (functionalKeyBackground != null) ? functionalKeyBackground + : mKeyBackground; + final Drawable spacebarBackground = keyboardViewAttr.getDrawable( + R.styleable.KeyboardView_spacebarBackground); + mSpacebarBackground = (spacebarBackground != null) ? spacebarBackground : mKeyBackground; + mSpacebarIconWidthRatio = keyboardViewAttr.getFloat( + R.styleable.KeyboardView_spacebarIconWidthRatio, 1.0f); mKeyLabelHorizontalPadding = keyboardViewAttr.getDimensionPixelOffset( R.styleable.KeyboardView_keyLabelHorizontalPadding, 0); mKeyHintLetterPadding = keyboardViewAttr.getDimension( @@ -136,7 +146,7 @@ public class KeyboardView extends View { mKeyShiftedLetterHintPadding = keyboardViewAttr.getDimension( R.styleable.KeyboardView_keyShiftedLetterHintPadding, 0.0f); mKeyTextShadowRadius = keyboardViewAttr.getFloat( - R.styleable.KeyboardView_keyTextShadowRadius, 0.0f); + R.styleable.KeyboardView_keyTextShadowRadius, KET_TEXT_SHADOW_RADIUS_DISABLED); mVerticalCorrection = keyboardViewAttr.getDimension( R.styleable.KeyboardView_verticalCorrection, 0.0f); keyboardViewAttr.recycle(); @@ -149,6 +159,10 @@ public class KeyboardView extends View { mPaint.setAntiAlias(true); } + public KeyVisualAttributes getKeyVisualAttribute() { + return mKeyVisualAttributes; + } + private static void blendAlpha(final Paint paint, final int alpha) { final int color = paint.getColor(); paint.setARGB((paint.getAlpha() * alpha) / Constants.Color.ALPHA_OPAQUE, @@ -170,7 +184,6 @@ public class KeyboardView extends View { */ public void setKeyboard(final Keyboard keyboard) { mKeyboard = keyboard; - LatinImeLogger.onSetKeyboard(keyboard); final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap; mKeyDrawParams.updateParams(keyHeight, mKeyVisualAttributes); mKeyDrawParams.updateParams(keyHeight, keyboard.mKeyVisualAttributes); @@ -288,7 +301,7 @@ public class KeyboardView extends View { // TODO: Confirm if it's really required to draw all keys when hardware acceleration is on. if (drawAllKeys || isHardwareAccelerated) { // Draw all keys. - for (final Key key : mKeyboard.getKeys()) { + for (final Key key : mKeyboard.getSortedKeys()) { onDrawKey(key, canvas, paint); } } else { @@ -300,13 +313,6 @@ public class KeyboardView extends View { } } - // Research Logging (Development Only Diagnostics) indicator. - // TODO: Reimplement using a keyboard background image specific to the ResearchLogger, - // and remove this call. - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.getInstance().paintIndicator(this, paint, canvas, width, height); - } - mInvalidatedKeys.clear(); mInvalidateAllKeys = false; } @@ -322,7 +328,9 @@ public class KeyboardView extends View { params.mAnimAlpha = Constants.Color.ALPHA_OPAQUE; if (!key.isSpacer()) { - onDrawKeyBackground(key, canvas); + final Drawable background = key.selectBackgroundDrawable( + mKeyBackground, mFunctionalKeyBackground, mSpacebarBackground); + onDrawKeyBackground(key, canvas, background); } onDrawKeyTopVisuals(key, canvas, paint, params); @@ -330,24 +338,19 @@ public class KeyboardView extends View { } // Draw key background. - protected void onDrawKeyBackground(final Key key, final Canvas canvas) { + protected void onDrawKeyBackground(final Key key, final Canvas canvas, + final Drawable background) { final Rect padding = mKeyBackgroundPadding; final int bgWidth = key.getDrawWidth() + padding.left + padding.right; final int bgHeight = key.getHeight() + padding.top + padding.bottom; final int bgX = -padding.left; final int bgY = -padding.top; - final int[] drawableState = key.getCurrentDrawableState(); - final Drawable background = mKeyBackground; - background.setState(drawableState); final Rect bounds = background.getBounds(); if (bgWidth != bounds.right || bgHeight != bounds.bottom) { background.setBounds(0, 0, bgWidth, bgHeight); } canvas.translate(bgX, bgY); background.draw(canvas); - if (LatinImeLogger.sVISUALDEBUG) { - drawRectangle(canvas, 0.0f, 0.0f, bgWidth, bgHeight, 0x80c00000, new Paint()); - } canvas.translate(-bgX, -bgY); } @@ -359,10 +362,6 @@ public class KeyboardView extends View { final float centerX = keyWidth * 0.5f; final float centerY = keyHeight * 0.5f; - if (LatinImeLogger.sVISUALDEBUG) { - drawRectangle(canvas, 0.0f, 0.0f, keyWidth, keyHeight, 0x800000c0, new Paint()); - } - // Draw key label. final Drawable icon = key.getIcon(mKeyboard.mIconsSet, params.mAnimAlpha); float positionX = centerX; @@ -370,10 +369,8 @@ public class KeyboardView extends View { if (label != null) { paint.setTypeface(key.selectTypeface(params)); paint.setTextSize(key.selectTextSize(params)); - final float labelCharHeight = TypefaceUtils.getCharHeight( - KEY_LABEL_REFERENCE_CHAR, paint); - final float labelCharWidth = TypefaceUtils.getCharWidth( - KEY_LABEL_REFERENCE_CHAR, paint); + final float labelCharHeight = TypefaceUtils.getReferenceCharHeight(paint); + final float labelCharWidth = TypefaceUtils.getReferenceCharWidth(paint); // Vertical label text alignment. final float baseline = centerY + labelCharHeight / 2.0f; @@ -391,12 +388,12 @@ public class KeyboardView extends View { positionX = centerX - labelCharWidth * 7.0f / 4.0f; paint.setTextAlign(Align.LEFT); } else if (key.hasLabelWithIconLeft() && icon != null) { - labelWidth = TypefaceUtils.getLabelWidth(label, paint) + icon.getIntrinsicWidth() + labelWidth = TypefaceUtils.getStringWidth(label, paint) + icon.getIntrinsicWidth() + LABEL_ICON_MARGIN * keyWidth; positionX = centerX + labelWidth / 2.0f; paint.setTextAlign(Align.RIGHT); } else if (key.hasLabelWithIconRight() && icon != null) { - labelWidth = TypefaceUtils.getLabelWidth(label, paint) + icon.getIntrinsicWidth() + labelWidth = TypefaceUtils.getStringWidth(label, paint) + icon.getIntrinsicWidth() + LABEL_ICON_MARGIN * keyWidth; positionX = centerX - labelWidth / 2.0f; paint.setTextAlign(Align.LEFT); @@ -404,23 +401,34 @@ public class KeyboardView extends View { positionX = centerX; paint.setTextAlign(Align.CENTER); } - if (key.needsXScale()) { - paint.setTextScaleX(Math.min(1.0f, - (keyWidth * MAX_LABEL_RATIO) / TypefaceUtils.getLabelWidth(label, paint))); + if (key.needsAutoXScale()) { + final float ratio = Math.min(1.0f, (keyWidth * MAX_LABEL_RATIO) / + TypefaceUtils.getStringWidth(label, paint)); + if (key.needsAutoScale()) { + final float autoSize = paint.getTextSize() * ratio; + paint.setTextSize(autoSize); + } else { + paint.setTextScaleX(ratio); + } } - paint.setColor(key.selectTextColor(params)); if (key.isEnabled()) { - // Set a drop shadow for the text - paint.setShadowLayer(mKeyTextShadowRadius, 0.0f, 0.0f, params.mTextShadowColor); + paint.setColor(key.selectTextColor(params)); + // Set a drop shadow for the text if the shadow radius is positive value. + if (mKeyTextShadowRadius > 0.0f) { + paint.setShadowLayer(mKeyTextShadowRadius, 0.0f, 0.0f, params.mTextShadowColor); + } else { + paint.clearShadowLayer(); + } } else { // Make label invisible paint.setColor(Color.TRANSPARENT); + paint.clearShadowLayer(); } blendAlpha(paint, params.mAnimAlpha); canvas.drawText(label, 0, label.length(), positionX, baseline, paint); // Turn off drop shadow and reset x-scale. - paint.setShadowLayer(0.0f, 0.0f, 0.0f, Color.TRANSPARENT); + paint.clearShadowLayer(); paint.setTextScaleX(1.0f); if (icon != null) { @@ -435,12 +443,6 @@ public class KeyboardView extends View { drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight); } } - - if (LatinImeLogger.sVISUALDEBUG) { - final Paint line = new Paint(); - drawHorizontalLine(canvas, baseline, keyWidth, 0xc0008000, line); - drawVerticalLine(canvas, positionX, keyHeight, 0xc0800080, line); - } } // Draw hint label. @@ -451,67 +453,57 @@ public class KeyboardView extends View { // TODO: Should add a way to specify type face for hint letters paint.setTypeface(Typeface.DEFAULT_BOLD); blendAlpha(paint, params.mAnimAlpha); + final float labelCharHeight = TypefaceUtils.getReferenceCharHeight(paint); + final float labelCharWidth = TypefaceUtils.getReferenceCharWidth(paint); + final KeyVisualAttributes visualAttr = key.getVisualAttributes(); + final float adjustmentY = (visualAttr == null) ? 0.0f + : visualAttr.mHintLabelVerticalAdjustment * labelCharHeight; final float hintX, hintY; if (key.hasHintLabel()) { // The hint label is placed just right of the key label. Used mainly on // "phone number" layout. // TODO: Generalize the following calculations. - hintX = positionX - + TypefaceUtils.getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) * 2.0f; - hintY = centerY - + TypefaceUtils.getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint) / 2.0f; + hintX = positionX + labelCharWidth * 2.0f; + hintY = centerY + labelCharHeight / 2.0f; paint.setTextAlign(Align.LEFT); } else if (key.hasShiftedLetterHint()) { // The hint label is placed at top-right corner of the key. Used mainly on tablet. - hintX = keyWidth - mKeyShiftedLetterHintPadding - - TypefaceUtils.getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2.0f; + hintX = keyWidth - mKeyShiftedLetterHintPadding - labelCharWidth / 2.0f; paint.getFontMetrics(mFontMetrics); hintY = -mFontMetrics.top; paint.setTextAlign(Align.CENTER); } else { // key.hasHintLetter() // The hint letter is placed at top-right corner of the key. Used mainly on phone. - final float keyNumericHintLabelReferenceCharWidth = - TypefaceUtils.getCharWidth(KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR, paint); - final float keyHintLabelStringWidth = - TypefaceUtils.getStringWidth(hintLabel, paint); + final float hintDigitWidth = TypefaceUtils.getReferenceDigitWidth(paint); + final float hintLabelWidth = TypefaceUtils.getStringWidth(hintLabel, paint); hintX = keyWidth - mKeyHintLetterPadding - - Math.max(keyNumericHintLabelReferenceCharWidth, keyHintLabelStringWidth) - / 2.0f; + - Math.max(hintDigitWidth, hintLabelWidth) / 2.0f; hintY = -paint.ascent(); paint.setTextAlign(Align.CENTER); } - canvas.drawText(hintLabel, 0, hintLabel.length(), hintX, hintY, paint); - - if (LatinImeLogger.sVISUALDEBUG) { - final Paint line = new Paint(); - drawHorizontalLine(canvas, (int)hintY, keyWidth, 0xc0808000, line); - drawVerticalLine(canvas, (int)hintX, keyHeight, 0xc0808000, line); - } + canvas.drawText(hintLabel, 0, hintLabel.length(), hintX, hintY + adjustmentY, paint); } // Draw key icon. if (label == null && icon != null) { - final int iconWidth = Math.min(icon.getIntrinsicWidth(), keyWidth); + final int iconWidth; + if (key.getCode() == Constants.CODE_SPACE && icon instanceof NinePatchDrawable) { + iconWidth = (int)(keyWidth * mSpacebarIconWidthRatio); + } else { + iconWidth = Math.min(icon.getIntrinsicWidth(), keyWidth); + } final int iconHeight = icon.getIntrinsicHeight(); - final int iconX, alignX; - final int iconY = (keyHeight - iconHeight) / 2; + final int iconY = key.isAlignButtom() ? keyHeight - iconHeight + : (keyHeight - iconHeight) / 2; + final int iconX; if (key.isAlignLeft()) { iconX = mKeyLabelHorizontalPadding; - alignX = iconX; } else if (key.isAlignRight()) { iconX = keyWidth - mKeyLabelHorizontalPadding - iconWidth; - alignX = iconX + iconWidth; } else { // Align center iconX = (keyWidth - iconWidth) / 2; - alignX = iconX + iconWidth / 2; } drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight); - - if (LatinImeLogger.sVISUALDEBUG) { - final Paint line = new Paint(); - drawVerticalLine(canvas, alignX, keyHeight, 0xc0800080, line); - drawRectangle(canvas, iconX, iconY, iconWidth, iconHeight, 0x80c00000, line); - } } if (key.hasPopupHint() && key.getMoreKeys() != null) { @@ -530,15 +522,9 @@ public class KeyboardView extends View { paint.setColor(params.mHintLabelColor); paint.setTextAlign(Align.CENTER); final float hintX = keyWidth - mKeyHintLetterPadding - - TypefaceUtils.getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2.0f; + - TypefaceUtils.getReferenceCharWidth(paint) / 2.0f; final float hintY = keyHeight - mKeyPopupHintLetterPadding; canvas.drawText(POPUP_HINT_CHAR, hintX, hintY, paint); - - if (LatinImeLogger.sVISUALDEBUG) { - final Paint line = new Paint(); - drawHorizontalLine(canvas, (int)hintY, keyWidth, 0xc0808000, line); - drawVerticalLine(canvas, (int)hintX, keyHeight, 0xc0808000, line); - } } protected static void drawIcon(final Canvas canvas, final Drawable icon, final int x, @@ -549,32 +535,6 @@ public class KeyboardView extends View { canvas.translate(-x, -y); } - 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.0f, y, w, y, 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.0f, x, h, 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); - canvas.translate(x, y); - canvas.drawRect(0.0f, 0.0f, w, h, paint); - canvas.translate(-x, -y); - } - public Paint newLabelPaint(final Key key) { final Paint paint = new Paint(); paint.setAntiAlias(true); @@ -582,6 +542,7 @@ public class KeyboardView extends View { paint.setTypeface(mKeyDrawParams.mTypeface); paint.setTextSize(mKeyDrawParams.mLabelSize); } else { + paint.setColor(key.selectTextColor(mKeyDrawParams)); paint.setTypeface(key.selectTypeface(mKeyDrawParams)); paint.setTextSize(key.selectTextSize(mKeyDrawParams)); } diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java index 13db47004..702efb3d7 100644 --- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java @@ -27,60 +27,47 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Align; import android.graphics.Typeface; -import android.graphics.drawable.Drawable; -import android.os.Message; -import android.os.SystemClock; import android.preference.PreferenceManager; import android.util.AttributeSet; -import android.util.DisplayMetrics; import android.util.Log; -import android.util.SparseArray; -import android.util.TypedValue; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; -import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.inputmethod.InputMethodSubtype; -import android.widget.TextView; import com.android.inputmethod.accessibility.AccessibilityUtils; -import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy; +import com.android.inputmethod.accessibility.MainKeyboardAccessibilityDelegate; import com.android.inputmethod.annotations.ExternallyReferenced; -import com.android.inputmethod.keyboard.PointerTracker.DrawingProxy; -import com.android.inputmethod.keyboard.PointerTracker.TimerProxy; -import com.android.inputmethod.keyboard.internal.GestureFloatingPreviewText; -import com.android.inputmethod.keyboard.internal.GestureTrailsPreview; +import com.android.inputmethod.keyboard.internal.DrawingHandler; +import com.android.inputmethod.keyboard.internal.DrawingPreviewPlacerView; +import com.android.inputmethod.keyboard.internal.GestureFloatingTextDrawingPreview; +import com.android.inputmethod.keyboard.internal.GestureTrailsDrawingPreview; import com.android.inputmethod.keyboard.internal.KeyDrawParams; +import com.android.inputmethod.keyboard.internal.KeyPreviewChoreographer; import com.android.inputmethod.keyboard.internal.KeyPreviewDrawParams; +import com.android.inputmethod.keyboard.internal.LanguageOnSpacebarHelper; +import com.android.inputmethod.keyboard.internal.MoreKeySpec; import com.android.inputmethod.keyboard.internal.NonDistinctMultitouchHelper; -import com.android.inputmethod.keyboard.internal.PreviewPlacerView; -import com.android.inputmethod.keyboard.internal.SlidingKeyInputPreview; +import com.android.inputmethod.keyboard.internal.SlidingKeyInputDrawingPreview; +import com.android.inputmethod.keyboard.internal.TimerHandler; import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.SuggestedWords; -import com.android.inputmethod.latin.define.ProductionFlag; import com.android.inputmethod.latin.settings.DebugSettings; -import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.CoordinateUtils; -import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper; -import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; +import com.android.inputmethod.latin.utils.SpacebarLanguageUtils; import com.android.inputmethod.latin.utils.TypefaceUtils; -import com.android.inputmethod.latin.utils.UsabilityStudyLogUtils; -import com.android.inputmethod.latin.utils.ViewLayoutUtils; -import com.android.inputmethod.research.ResearchLogger; import java.util.WeakHashMap; /** * A view that is responsible for detecting key presses and touch movements. * - * @attr ref R.styleable#MainKeyboardView_autoCorrectionSpacebarLedEnabled - * @attr ref R.styleable#MainKeyboardView_autoCorrectionSpacebarLedIcon - * @attr ref R.styleable#MainKeyboardView_spacebarTextRatio - * @attr ref R.styleable#MainKeyboardView_spacebarTextColor - * @attr ref R.styleable#MainKeyboardView_spacebarTextShadowColor + * @attr ref R.styleable#MainKeyboardView_languageOnSpacebarTextRatio + * @attr ref R.styleable#MainKeyboardView_languageOnSpacebarTextColor + * @attr ref R.styleable#MainKeyboardView_languageOnSpacebarTextShadowRadius + * @attr ref R.styleable#MainKeyboardView_languageOnSpacebarTextShadowColor * @attr ref R.styleable#MainKeyboardView_languageOnSpacebarFinalAlpha * @attr ref R.styleable#MainKeyboardView_languageOnSpacebarFadeoutAnimator * @attr ref R.styleable#MainKeyboardView_altCodeKeyWhileTypingFadeoutAnimator @@ -88,7 +75,7 @@ import java.util.WeakHashMap; * @attr ref R.styleable#MainKeyboardView_keyHysteresisDistance * @attr ref R.styleable#MainKeyboardView_touchNoiseThresholdTime * @attr ref R.styleable#MainKeyboardView_touchNoiseThresholdDistance - * @attr ref R.styleable#MainKeyboardView_slidingKeyInputEnable + * @attr ref R.styleable#MainKeyboardView_keySelectionByDraggingFinger * @attr ref R.styleable#MainKeyboardView_keyRepeatStartTimeout * @attr ref R.styleable#MainKeyboardView_keyRepeatInterval * @attr ref R.styleable#MainKeyboardView_longPressKeyTimeout @@ -114,61 +101,51 @@ import java.util.WeakHashMap; * @attr ref R.styleable#MainKeyboardView_gestureRecognitionSpeedThreshold * @attr ref R.styleable#MainKeyboardView_suppressKeyPreviewAfterBatchInputDuration */ -public final class MainKeyboardView extends KeyboardView implements PointerTracker.KeyEventHandler, - PointerTracker.DrawingProxy, MoreKeysPanel.Controller { +public final class MainKeyboardView extends KeyboardView implements PointerTracker.DrawingProxy, + MoreKeysPanel.Controller, DrawingHandler.Callbacks, TimerHandler.Callbacks { private static final String TAG = MainKeyboardView.class.getSimpleName(); /** Listener for {@link KeyboardActionListener}. */ private KeyboardActionListener mKeyboardActionListener; - /* Space key and its icons */ + /* Space key and its icon and background. */ private Key mSpaceKey; - private Drawable mSpaceIcon; // Stuff to draw language name on spacebar. private final int mLanguageOnSpacebarFinalAlpha; private ObjectAnimator mLanguageOnSpacebarFadeoutAnimator; - private boolean mNeedsToDisplayLanguage; + private int mLanguageOnSpacebarFormatType; private boolean mHasMultipleEnabledIMEsOrSubtypes; private int mLanguageOnSpacebarAnimAlpha = Constants.Color.ALPHA_OPAQUE; - private final float mSpacebarTextRatio; - private float mSpacebarTextSize; - private final int mSpacebarTextColor; - private final int mSpacebarTextShadowColor; + private final float mLanguageOnSpacebarTextRatio; + private float mLanguageOnSpacebarTextSize; + private final int mLanguageOnSpacebarTextColor; + private final float mLanguageOnSpacebarTextShadowRadius; + private final int mLanguageOnSpacebarTextShadowColor; + private static final float LANGUAGE_ON_SPACEBAR_TEXT_SHADOW_RADIUS_DISABLED = -1.0f; // The minimum x-scale to fit the language name on spacebar. private static final float MINIMUM_XSCALE_OF_LANGUAGE_NAME = 0.8f; - // Stuff to draw auto correction LED on spacebar. - private boolean mAutoCorrectionSpacebarLedOn; - private final boolean mAutoCorrectionSpacebarLedEnabled; - private final Drawable mAutoCorrectionSpacebarLedIcon; - private static final int SPACE_LED_LENGTH_PERCENT = 80; // Stuff to draw altCodeWhileTyping keys. - private ObjectAnimator mAltCodeKeyWhileTypingFadeoutAnimator; - private ObjectAnimator mAltCodeKeyWhileTypingFadeinAnimator; + private final ObjectAnimator mAltCodeKeyWhileTypingFadeoutAnimator; + private final ObjectAnimator mAltCodeKeyWhileTypingFadeinAnimator; private int mAltCodeKeyWhileTypingAnimAlpha = Constants.Color.ALPHA_OPAQUE; - // Preview placer view - private final PreviewPlacerView mPreviewPlacerView; + // Drawing preview placer view + private final DrawingPreviewPlacerView mDrawingPreviewPlacerView; private final int[] mOriginCoords = CoordinateUtils.newInstance(); - private final GestureFloatingPreviewText mGestureFloatingPreviewText; - private final GestureTrailsPreview mGestureTrailsPreview; - private final SlidingKeyInputPreview mSlidingKeyInputPreview; + private final GestureFloatingTextDrawingPreview mGestureFloatingTextDrawingPreview; + private final GestureTrailsDrawingPreview mGestureTrailsDrawingPreview; + private final SlidingKeyInputDrawingPreview mSlidingKeyInputDrawingPreview; // Key preview - private final int mKeyPreviewLayoutId; - private final int mKeyPreviewOffset; - private final int mKeyPreviewHeight; - private final SparseArray<TextView> mKeyPreviewTexts = CollectionUtils.newSparseArray(); - private final KeyPreviewDrawParams mKeyPreviewDrawParams = new KeyPreviewDrawParams(); - private boolean mShowKeyPreviewPopup = true; - private int mKeyPreviewLingerTimeout; + private final KeyPreviewDrawParams mKeyPreviewDrawParams; + private final KeyPreviewChoreographer mKeyPreviewChoreographer; // More keys keyboard private final Paint mBackgroundDimAlphaPaint = new Paint(); private boolean mNeedsToDimEntireKeyboard; private final View mMoreKeysKeyboardContainer; - private final WeakHashMap<Key, Keyboard> mMoreKeysKeyboardCache = - CollectionUtils.newWeakHashMap(); + private final WeakHashMap<Key, Keyboard> mMoreKeysKeyboardCache = new WeakHashMap<>(); private final boolean mConfigShowMoreKeysKeyboardAtTouchedPoint; // More keys panel (used by both more keys keyboard and more suggestions view) // TODO: Consider extending to support multiple more keys panels @@ -178,244 +155,15 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack // TODO: Make this parameter customizable by user via settings. private int mGestureFloatingPreviewTextLingerTimeout; - private KeyDetector mKeyDetector; + private final KeyDetector mKeyDetector; private final NonDistinctMultitouchHelper mNonDistinctMultitouchHelper; - private final KeyTimerHandler mKeyTimerHandler; + private final TimerHandler mKeyTimerHandler; private final int mLanguageOnSpacebarHorizontalMargin; - private static final class KeyTimerHandler extends StaticInnerHandlerWrapper<MainKeyboardView> - implements TimerProxy { - private static final int MSG_TYPING_STATE_EXPIRED = 0; - private static final int MSG_REPEAT_KEY = 1; - private static final int MSG_LONGPRESS_KEY = 2; - private static final int MSG_DOUBLE_TAP_SHIFT_KEY = 3; - private static final int MSG_UPDATE_BATCH_INPUT = 4; - - private final int mIgnoreAltCodeKeyTimeout; - private final int mGestureRecognitionUpdateTime; - - public KeyTimerHandler(final MainKeyboardView outerInstance, - final TypedArray mainKeyboardViewAttr) { - super(outerInstance); - - mIgnoreAltCodeKeyTimeout = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_ignoreAltCodeKeyTimeout, 0); - mGestureRecognitionUpdateTime = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_gestureRecognitionUpdateTime, 0); - } - - @Override - public void handleMessage(final Message msg) { - final MainKeyboardView keyboardView = getOuterInstance(); - if (keyboardView == null) { - return; - } - final PointerTracker tracker = (PointerTracker) msg.obj; - switch (msg.what) { - case MSG_TYPING_STATE_EXPIRED: - startWhileTypingFadeinAnimation(keyboardView); - break; - case MSG_REPEAT_KEY: - tracker.onKeyRepeat(msg.arg1 /* code */, msg.arg2 /* repeatCount */); - break; - case MSG_LONGPRESS_KEY: - keyboardView.onLongPress(tracker); - break; - case MSG_UPDATE_BATCH_INPUT: - tracker.updateBatchInputByTimer(SystemClock.uptimeMillis()); - startUpdateBatchInputTimer(tracker); - break; - } - } - - @Override - public void startKeyRepeatTimer(final PointerTracker tracker, final int repeatCount, - final int delay) { - final Key key = tracker.getKey(); - if (key == null || delay == 0) { - return; - } - sendMessageDelayed( - obtainMessage(MSG_REPEAT_KEY, key.getCode(), repeatCount, tracker), delay); - } - - public void cancelKeyRepeatTimer() { - removeMessages(MSG_REPEAT_KEY); - } - - // TODO: Suppress layout changes in key repeat mode - public boolean isInKeyRepeat() { - return hasMessages(MSG_REPEAT_KEY); - } - - @Override - public void startLongPressTimer(final PointerTracker tracker, final int delay) { - cancelLongPressTimer(); - if (delay <= 0) return; - sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, tracker), delay); - } - - @Override - public void cancelLongPressTimer() { - removeMessages(MSG_LONGPRESS_KEY); - } - - private static void cancelAndStartAnimators(final ObjectAnimator animatorToCancel, - final ObjectAnimator animatorToStart) { - if (animatorToCancel == null || animatorToStart == null) { - // TODO: Stop using null as a no-operation animator. - return; - } - float startFraction = 0.0f; - if (animatorToCancel.isStarted()) { - animatorToCancel.cancel(); - startFraction = 1.0f - animatorToCancel.getAnimatedFraction(); - } - final long startTime = (long)(animatorToStart.getDuration() * startFraction); - animatorToStart.start(); - animatorToStart.setCurrentPlayTime(startTime); - } - - private static void startWhileTypingFadeinAnimation(final MainKeyboardView keyboardView) { - cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator, - keyboardView.mAltCodeKeyWhileTypingFadeinAnimator); - } - - private static void startWhileTypingFadeoutAnimation(final MainKeyboardView keyboardView) { - cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeinAnimator, - keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator); - } - - @Override - public void startTypingStateTimer(final Key typedKey) { - if (typedKey.isModifier() || typedKey.altCodeWhileTyping()) { - return; - } - - final boolean isTyping = isTypingState(); - removeMessages(MSG_TYPING_STATE_EXPIRED); - final MainKeyboardView keyboardView = getOuterInstance(); - - // When user hits the space or the enter key, just cancel the while-typing timer. - final int typedCode = typedKey.getCode(); - if (typedCode == Constants.CODE_SPACE || typedCode == Constants.CODE_ENTER) { - if (isTyping) { - startWhileTypingFadeinAnimation(keyboardView); - } - return; - } - - sendMessageDelayed( - obtainMessage(MSG_TYPING_STATE_EXPIRED), mIgnoreAltCodeKeyTimeout); - if (isTyping) { - return; - } - startWhileTypingFadeoutAnimation(keyboardView); - } - - @Override - public boolean isTypingState() { - return hasMessages(MSG_TYPING_STATE_EXPIRED); - } - - @Override - public void startDoubleTapShiftKeyTimer() { - sendMessageDelayed(obtainMessage(MSG_DOUBLE_TAP_SHIFT_KEY), - ViewConfiguration.getDoubleTapTimeout()); - } - - @Override - public void cancelDoubleTapShiftKeyTimer() { - removeMessages(MSG_DOUBLE_TAP_SHIFT_KEY); - } - - @Override - public boolean isInDoubleTapShiftKeyTimeout() { - return hasMessages(MSG_DOUBLE_TAP_SHIFT_KEY); - } - - @Override - public void cancelKeyTimers() { - cancelKeyRepeatTimer(); - cancelLongPressTimer(); - } - - @Override - public void startUpdateBatchInputTimer(final PointerTracker tracker) { - if (mGestureRecognitionUpdateTime <= 0) { - return; - } - removeMessages(MSG_UPDATE_BATCH_INPUT, tracker); - sendMessageDelayed(obtainMessage(MSG_UPDATE_BATCH_INPUT, tracker), - mGestureRecognitionUpdateTime); - } - - @Override - public void cancelUpdateBatchInputTimer(final PointerTracker tracker) { - removeMessages(MSG_UPDATE_BATCH_INPUT, tracker); - } - - @Override - public void cancelAllUpdateBatchInputTimers() { - removeMessages(MSG_UPDATE_BATCH_INPUT); - } - - public void cancelAllMessages() { - cancelKeyTimers(); - cancelAllUpdateBatchInputTimers(); - } - } - private final DrawingHandler mDrawingHandler = new DrawingHandler(this); - public static class DrawingHandler extends StaticInnerHandlerWrapper<MainKeyboardView> { - private static final int MSG_DISMISS_KEY_PREVIEW = 0; - private static final int MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1; - - public DrawingHandler(final MainKeyboardView outerInstance) { - super(outerInstance); - } - - @Override - public void handleMessage(final Message msg) { - final MainKeyboardView mainKeyboardView = getOuterInstance(); - if (mainKeyboardView == null) return; - final PointerTracker tracker = (PointerTracker) msg.obj; - switch (msg.what) { - case MSG_DISMISS_KEY_PREVIEW: - final TextView previewText = mainKeyboardView.mKeyPreviewTexts.get( - tracker.mPointerId); - if (previewText != null) { - previewText.setVisibility(INVISIBLE); - } - break; - case MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT: - mainKeyboardView.showGestureFloatingPreviewText(SuggestedWords.EMPTY); - break; - } - } - - public void dismissKeyPreview(final long delay, final PointerTracker tracker) { - sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, tracker), delay); - } - - public void cancelDismissKeyPreview(final PointerTracker tracker) { - removeMessages(MSG_DISMISS_KEY_PREVIEW, tracker); - } - - private void cancelAllDismissKeyPreviews() { - removeMessages(MSG_DISMISS_KEY_PREVIEW); - } - - public void dismissGestureFloatingPreviewText(final long delay) { - sendMessageDelayed(obtainMessage(MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT), delay); - } - - public void cancelAllMessages() { - cancelAllDismissKeyPreviews(); - } - } + private MainKeyboardAccessibilityDelegate mAccessibilityDelegate; public MainKeyboardView(final Context context, final AttributeSet attrs) { this(context, attrs, R.attr.mainKeyboardViewStyle); @@ -424,7 +172,26 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack public MainKeyboardView(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); - PointerTracker.init(getResources()); + mDrawingPreviewPlacerView = new DrawingPreviewPlacerView(context, attrs); + + final TypedArray mainKeyboardViewAttr = context.obtainStyledAttributes( + attrs, R.styleable.MainKeyboardView, defStyle, R.style.MainKeyboardView); + final int ignoreAltCodeKeyTimeout = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_ignoreAltCodeKeyTimeout, 0); + final int gestureRecognitionUpdateTime = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_gestureRecognitionUpdateTime, 0); + mKeyTimerHandler = new TimerHandler( + this, ignoreAltCodeKeyTimeout, gestureRecognitionUpdateTime); + + final float keyHysteresisDistance = mainKeyboardViewAttr.getDimension( + R.styleable.MainKeyboardView_keyHysteresisDistance, 0.0f); + final float keyHysteresisDistanceForSlidingModifier = mainKeyboardViewAttr.getDimension( + R.styleable.MainKeyboardView_keyHysteresisDistanceForSlidingModifier, 0.0f); + mKeyDetector = new KeyDetector( + keyHysteresisDistance, keyHysteresisDistanceForSlidingModifier); + + PointerTracker.init(mainKeyboardViewAttr, mKeyTimerHandler, this /* DrawingProxy */); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); final boolean forceNonDistinctMultitouch = prefs.getBoolean( DebugSettings.PREF_FORCE_NON_DISTINCT_MULTITOUCH, false); @@ -434,24 +201,19 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack mNonDistinctMultitouchHelper = hasDistinctMultitouch ? null : new NonDistinctMultitouchHelper(); - mPreviewPlacerView = new PreviewPlacerView(context, attrs); - - final TypedArray mainKeyboardViewAttr = context.obtainStyledAttributes( - attrs, R.styleable.MainKeyboardView, defStyle, R.style.MainKeyboardView); final int backgroundDimAlpha = mainKeyboardViewAttr.getInt( R.styleable.MainKeyboardView_backgroundDimAlpha, 0); mBackgroundDimAlphaPaint.setColor(Color.BLACK); mBackgroundDimAlphaPaint.setAlpha(backgroundDimAlpha); - mAutoCorrectionSpacebarLedEnabled = mainKeyboardViewAttr.getBoolean( - R.styleable.MainKeyboardView_autoCorrectionSpacebarLedEnabled, false); - mAutoCorrectionSpacebarLedIcon = mainKeyboardViewAttr.getDrawable( - R.styleable.MainKeyboardView_autoCorrectionSpacebarLedIcon); - mSpacebarTextRatio = mainKeyboardViewAttr.getFraction( - R.styleable.MainKeyboardView_spacebarTextRatio, 1, 1, 1.0f); - mSpacebarTextColor = mainKeyboardViewAttr.getColor( - R.styleable.MainKeyboardView_spacebarTextColor, 0); - mSpacebarTextShadowColor = mainKeyboardViewAttr.getColor( - R.styleable.MainKeyboardView_spacebarTextShadowColor, 0); + mLanguageOnSpacebarTextRatio = mainKeyboardViewAttr.getFraction( + R.styleable.MainKeyboardView_languageOnSpacebarTextRatio, 1, 1, 1.0f); + mLanguageOnSpacebarTextColor = mainKeyboardViewAttr.getColor( + R.styleable.MainKeyboardView_languageOnSpacebarTextColor, 0); + mLanguageOnSpacebarTextShadowRadius = mainKeyboardViewAttr.getFloat( + R.styleable.MainKeyboardView_languageOnSpacebarTextShadowRadius, + LANGUAGE_ON_SPACEBAR_TEXT_SHADOW_RADIUS_DISABLED); + mLanguageOnSpacebarTextShadowColor = mainKeyboardViewAttr.getColor( + R.styleable.MainKeyboardView_languageOnSpacebarTextShadowColor, 0); mLanguageOnSpacebarFinalAlpha = mainKeyboardViewAttr.getInt( R.styleable.MainKeyboardView_languageOnSpacebarFinalAlpha, Constants.Color.ALPHA_OPAQUE); @@ -462,24 +224,9 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack final int altCodeKeyWhileTypingFadeinAnimatorResId = mainKeyboardViewAttr.getResourceId( R.styleable.MainKeyboardView_altCodeKeyWhileTypingFadeinAnimator, 0); - final float keyHysteresisDistance = mainKeyboardViewAttr.getDimension( - R.styleable.MainKeyboardView_keyHysteresisDistance, 0.0f); - final float keyHysteresisDistanceForSlidingModifier = mainKeyboardViewAttr.getDimension( - R.styleable.MainKeyboardView_keyHysteresisDistanceForSlidingModifier, 0.0f); - mKeyDetector = new KeyDetector( - keyHysteresisDistance, keyHysteresisDistanceForSlidingModifier); - mKeyTimerHandler = new KeyTimerHandler(this, mainKeyboardViewAttr); - mKeyPreviewOffset = mainKeyboardViewAttr.getDimensionPixelOffset( - R.styleable.MainKeyboardView_keyPreviewOffset, 0); - mKeyPreviewHeight = mainKeyboardViewAttr.getDimensionPixelSize( - R.styleable.MainKeyboardView_keyPreviewHeight, 0); - mKeyPreviewLingerTimeout = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_keyPreviewLingerTimeout, 0); - mKeyPreviewLayoutId = mainKeyboardViewAttr.getResourceId( - R.styleable.MainKeyboardView_keyPreviewLayout, 0); - if (mKeyPreviewLayoutId == 0) { - mShowKeyPreviewPopup = false; - } + mKeyPreviewDrawParams = new KeyPreviewDrawParams(mainKeyboardViewAttr); + mKeyPreviewChoreographer = new KeyPreviewChoreographer(mKeyPreviewDrawParams); + final int moreKeysKeyboardLayoutId = mainKeyboardViewAttr.getResourceId( R.styleable.MainKeyboardView_moreKeysKeyboardLayout, 0); mConfigShowMoreKeysKeyboardAtTouchedPoint = mainKeyboardViewAttr.getBoolean( @@ -487,19 +234,16 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack mGestureFloatingPreviewTextLingerTimeout = mainKeyboardViewAttr.getInt( R.styleable.MainKeyboardView_gestureFloatingPreviewTextLingerTimeout, 0); - PointerTracker.setParameters(mainKeyboardViewAttr); - mGestureFloatingPreviewText = new GestureFloatingPreviewText( - mPreviewPlacerView, mainKeyboardViewAttr); - mPreviewPlacerView.addPreview(mGestureFloatingPreviewText); + mGestureFloatingTextDrawingPreview = new GestureFloatingTextDrawingPreview( + mainKeyboardViewAttr); + mGestureFloatingTextDrawingPreview.setDrawingView(mDrawingPreviewPlacerView); - mGestureTrailsPreview = new GestureTrailsPreview( - mPreviewPlacerView, mainKeyboardViewAttr); - mPreviewPlacerView.addPreview(mGestureTrailsPreview); + mGestureTrailsDrawingPreview = new GestureTrailsDrawingPreview(mainKeyboardViewAttr); + mGestureTrailsDrawingPreview.setDrawingView(mDrawingPreviewPlacerView); - mSlidingKeyInputPreview = new SlidingKeyInputPreview( - mPreviewPlacerView, mainKeyboardViewAttr); - mPreviewPlacerView.addPreview(mSlidingKeyInputPreview); + mSlidingKeyInputDrawingPreview = new SlidingKeyInputDrawingPreview(mainKeyboardViewAttr); + mSlidingKeyInputDrawingPreview.setDrawingView(mDrawingPreviewPlacerView); mainKeyboardViewAttr.recycle(); mMoreKeysKeyboardContainer = LayoutInflater.from(getContext()) @@ -513,14 +257,14 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack mKeyboardActionListener = KeyboardActionListener.EMPTY_LISTENER; - mLanguageOnSpacebarHorizontalMargin = - (int) getResources().getDimension(R.dimen.language_on_spacebar_horizontal_margin); + mLanguageOnSpacebarHorizontalMargin = (int)getResources().getDimension( + R.dimen.config_language_on_spacebar_horizontal_margin); } @Override public void setHardwareAcceleratedDrawingEnabled(final boolean enabled) { super.setHardwareAcceleratedDrawingEnabled(enabled); - mPreviewPlacerView.setHardwareAcceleratedDrawingEnabled(enabled); + mDrawingPreviewPlacerView.setHardwareAcceleratedDrawingEnabled(enabled); } private ObjectAnimator loadObjectAnimator(final int resId, final Object target) { @@ -536,6 +280,35 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack return animator; } + private static void cancelAndStartAnimators(final ObjectAnimator animatorToCancel, + final ObjectAnimator animatorToStart) { + if (animatorToCancel == null || animatorToStart == null) { + // TODO: Stop using null as a no-operation animator. + return; + } + float startFraction = 0.0f; + if (animatorToCancel.isStarted()) { + animatorToCancel.cancel(); + startFraction = 1.0f - animatorToCancel.getAnimatedFraction(); + } + final long startTime = (long)(animatorToStart.getDuration() * startFraction); + animatorToStart.start(); + animatorToStart.setCurrentPlayTime(startTime); + } + + // Implements {@link TimerHander.Callbacks} method. + @Override + public void startWhileTypingFadeinAnimation() { + cancelAndStartAnimators( + mAltCodeKeyWhileTypingFadeoutAnimator, mAltCodeKeyWhileTypingFadeinAnimator); + } + + @Override + public void startWhileTypingFadeoutAnimation() { + cancelAndStartAnimators( + mAltCodeKeyWhileTypingFadeinAnimator, mAltCodeKeyWhileTypingFadeoutAnimator); + } + @ExternallyReferenced public int getLanguageOnSpacebarAnimAlpha() { return mLanguageOnSpacebarAnimAlpha; @@ -573,28 +346,16 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack PointerTracker.setKeyboardActionListener(listener); } - /** - * Returns the {@link KeyboardActionListener} object. - * @return the listener attached to this keyboard - */ - @Override - public KeyboardActionListener getKeyboardActionListener() { - return mKeyboardActionListener; - } - - @Override - public KeyDetector getKeyDetector() { - return mKeyDetector; + // TODO: We should reconsider which coordinate system should be used to represent keyboard + // event. + public int getKeyX(final int x) { + return Constants.isValidCoordinate(x) ? mKeyDetector.getTouchX(x) : x; } - @Override - public DrawingProxy getDrawingProxy() { - return this; - } - - @Override - public TimerProxy getTimerProxy() { - return mKeyTimerHandler; + // TODO: We should reconsider which coordinate system should be used to represent keyboard + // event. + public int getKeyY(final int y) { + return Constants.isValidCoordinate(y) ? mKeyDetector.getTouchY(y) : y; } /** @@ -607,7 +368,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack @Override public void setKeyboard(final Keyboard keyboard) { // Remove any pending messages, except dismissing preview and key repeat. - mKeyTimerHandler.cancelLongPressTimer(); + mKeyTimerHandler.cancelLongPressTimers(); super.setKeyboard(keyboard); mKeyDetector.setKeyboard( keyboard, -getPaddingLeft(), -getPaddingTop() + getVerticalCorrection()); @@ -615,18 +376,17 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack mMoreKeysKeyboardCache.clear(); mSpaceKey = keyboard.getKey(Constants.CODE_SPACE); - mSpaceIcon = (mSpaceKey != null) - ? mSpaceKey.getIcon(keyboard.mIconsSet, Constants.Color.ALPHA_OPAQUE) : null; final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap; - mSpacebarTextSize = keyHeight * mSpacebarTextRatio; - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - final int orientation = getContext().getResources().getConfiguration().orientation; - ResearchLogger.mainKeyboardView_setKeyboard(keyboard, orientation); - } + mLanguageOnSpacebarTextSize = keyHeight * mLanguageOnSpacebarTextRatio; - // This always needs to be set since the accessibility state can - // potentially change without the keyboard being set again. - AccessibleKeyboardViewProxy.getInstance().setKeyboard(); + if (AccessibilityUtils.getInstance().isAccessibilityEnabled()) { + if (mAccessibilityDelegate == null) { + mAccessibilityDelegate = new MainKeyboardAccessibilityDelegate(this, mKeyDetector); + } + mAccessibilityDelegate.setKeyboard(keyboard); + } else { + mAccessibilityDelegate = null; + } } /** @@ -637,26 +397,21 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack * @see #isKeyPreviewPopupEnabled() */ public void setKeyPreviewPopupEnabled(final boolean previewEnabled, final int delay) { - mShowKeyPreviewPopup = previewEnabled; - mKeyPreviewLingerTimeout = delay; + mKeyPreviewDrawParams.setPopupEnabled(previewEnabled, delay); + } + + public void setKeyPreviewAnimationParams(final float showUpStartScale, final int showUpDuration, + final float dismissEndScale, final int dismissDuration) { + mKeyPreviewDrawParams.setAnimationParams( + showUpStartScale, showUpDuration, dismissEndScale, dismissDuration); } private void locatePreviewPlacerView() { - if (mPreviewPlacerView.getParent() != null) { - return; - } - final int width = getWidth(); - final int height = getHeight(); - if (width == 0 || height == 0) { - // In transient state. - return; - } getLocationInWindow(mOriginCoords); - final DisplayMetrics dm = getResources().getDisplayMetrics(); - if (CoordinateUtils.y(mOriginCoords) < dm.heightPixels / 4) { - // In transient state. - return; - } + mDrawingPreviewPlacerView.setKeyboardViewGeometry(mOriginCoords, getWidth(), getHeight()); + } + + private void installPreviewPlacerView() { final View rootView = getRootView(); if (rootView == null) { Log.w(TAG, "Cannot find root view"); @@ -665,191 +420,85 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack final ViewGroup windowContentView = (ViewGroup)rootView.findViewById(android.R.id.content); // Note: It'd be very weird if we get null by android.R.id.content. if (windowContentView == null) { - Log.w(TAG, "Cannot find android.R.id.content view to add PreviewPlacerView"); - } else { - windowContentView.addView(mPreviewPlacerView); - mPreviewPlacerView.setKeyboardViewGeometry(mOriginCoords, width, height); - } - } - - /** - * Returns the enabled state of the key feedback preview - * @return whether or not the key feedback preview is enabled - * @see #setKeyPreviewPopupEnabled(boolean, int) - */ - public boolean isKeyPreviewPopupEnabled() { - return mShowKeyPreviewPopup; - } - - private void addKeyPreview(final TextView keyPreview) { - locatePreviewPlacerView(); - mPreviewPlacerView.addView( - keyPreview, ViewLayoutUtils.newLayoutParam(mPreviewPlacerView, 0, 0)); - } - - private TextView getKeyPreviewText(final int pointerId) { - TextView previewText = mKeyPreviewTexts.get(pointerId); - if (previewText != null) { - return previewText; - } - final Context context = getContext(); - if (mKeyPreviewLayoutId != 0) { - previewText = (TextView)LayoutInflater.from(context).inflate(mKeyPreviewLayoutId, null); - } else { - previewText = new TextView(context); + Log.w(TAG, "Cannot find android.R.id.content view to add DrawingPreviewPlacerView"); + return; } - mKeyPreviewTexts.put(pointerId, previewText); - return previewText; + windowContentView.addView(mDrawingPreviewPlacerView); } - private void dismissAllKeyPreviews() { - final int pointerCount = mKeyPreviewTexts.size(); - for (int id = 0; id < pointerCount; id++) { - final TextView previewText = mKeyPreviewTexts.get(id); - if (previewText != null) { - previewText.setVisibility(INVISIBLE); - } - } + // Implements {@link DrawingHandler.Callbacks} method. + @Override + public void dismissAllKeyPreviews() { + mKeyPreviewChoreographer.dismissAllKeyPreviews(); PointerTracker.setReleasedKeyGraphicsToAllKeys(); } - // Background state set - private static final int[][][] KEY_PREVIEW_BACKGROUND_STATE_TABLE = { - { // STATE_MIDDLE - EMPTY_STATE_SET, - { R.attr.state_has_morekeys } - }, - { // STATE_LEFT - { R.attr.state_left_edge }, - { R.attr.state_left_edge, R.attr.state_has_morekeys } - }, - { // STATE_RIGHT - { R.attr.state_right_edge }, - { R.attr.state_right_edge, R.attr.state_has_morekeys } - } - }; - private static final int STATE_MIDDLE = 0; - private static final int STATE_LEFT = 1; - private static final int STATE_RIGHT = 2; - private static final int STATE_NORMAL = 0; - private static final int STATE_HAS_MOREKEYS = 1; - @Override - public void showKeyPreview(final PointerTracker tracker) { - final KeyPreviewDrawParams previewParams = mKeyPreviewDrawParams; - final Keyboard keyboard = getKeyboard(); - if (!mShowKeyPreviewPopup) { - previewParams.mPreviewVisibleOffset = -keyboard.mVerticalGap; + public void showKeyPreview(final Key key) { + // If the key is invalid or has no key preview, we must not show key preview. + if (key == null || key.noKeyPreview()) { return; } - - final TextView previewText = getKeyPreviewText(tracker.mPointerId); - // If the key preview has no parent view yet, add it to the ViewGroup which can place - // key preview absolutely in SoftInputWindow. - if (previewText.getParent() == null) { - addKeyPreview(previewText); - } - - mDrawingHandler.cancelDismissKeyPreview(tracker); - final Key key = tracker.getKey(); - // 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) { + final Keyboard keyboard = getKeyboard(); + if (keyboard == null) { return; } - - final KeyDrawParams drawParams = mKeyDrawParams; - previewText.setTextColor(drawParams.mPreviewTextColor); - final Drawable background = previewText.getBackground(); - final String label = key.getPreviewLabel(); - // What we show as preview should match what we show on a key top in onDraw(). - if (label != null) { - // TODO Should take care of temporaryShiftLabel here. - previewText.setCompoundDrawables(null, null, null, null); - previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, - key.selectPreviewTextSize(drawParams)); - previewText.setTypeface(key.selectPreviewTypeface(drawParams)); - previewText.setText(label); - } else { - previewText.setCompoundDrawables(null, null, null, - key.getPreviewIcon(keyboard.mIconsSet)); - previewText.setText(null); + final KeyPreviewDrawParams previewParams = mKeyPreviewDrawParams; + if (!previewParams.isPopupEnabled()) { + previewParams.setVisibleOffset(-keyboard.mVerticalGap); + return; } - previewText.measure( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - final int keyDrawWidth = key.getDrawWidth(); - final int previewWidth = previewText.getMeasuredWidth(); - final int previewHeight = mKeyPreviewHeight; - // The width and height of visible part of the key preview background. The content marker - // of the background 9-patch have to cover the visible part of the background. - previewParams.mPreviewVisibleWidth = previewWidth - previewText.getPaddingLeft() - - previewText.getPaddingRight(); - previewParams.mPreviewVisibleHeight = previewHeight - previewText.getPaddingTop() - - previewText.getPaddingBottom(); - // The distance between the top edge of the parent key and the bottom of the visible part - // of the key preview background. - previewParams.mPreviewVisibleOffset = mKeyPreviewOffset - previewText.getPaddingBottom(); + locatePreviewPlacerView(); getLocationInWindow(mOriginCoords); - // The key preview is horizontally aligned with the center of the visible part of the - // parent key. If it doesn't fit in this {@link KeyboardView}, it is moved inward to fit and - // the left/right background is used if such background is specified. - final int statePosition; - int previewX = key.getDrawX() - (previewWidth - keyDrawWidth) / 2 - + CoordinateUtils.x(mOriginCoords); - if (previewX < 0) { - previewX = 0; - statePosition = STATE_LEFT; - } else if (previewX > getWidth() - previewWidth) { - previewX = getWidth() - previewWidth; - statePosition = STATE_RIGHT; - } else { - statePosition = STATE_MIDDLE; - } - // The key preview is placed vertically above the top edge of the parent key with an - // arbitrary offset. - final int previewY = key.getY() - previewHeight + mKeyPreviewOffset - + CoordinateUtils.y(mOriginCoords); - - if (background != null) { - final int hasMoreKeys = (key.getMoreKeys() != null) ? STATE_HAS_MOREKEYS : STATE_NORMAL; - background.setState(KEY_PREVIEW_BACKGROUND_STATE_TABLE[statePosition][hasMoreKeys]); - } - ViewLayoutUtils.placeViewAt( - previewText, previewX, previewY, previewWidth, previewHeight); - previewText.setVisibility(VISIBLE); + mKeyPreviewChoreographer.placeAndShowKeyPreview(key, keyboard.mIconsSet, mKeyDrawParams, + getWidth(), mOriginCoords, mDrawingPreviewPlacerView, isHardwareAccelerated()); } + // Implements {@link TimerHandler.Callbacks} method. @Override - public void dismissKeyPreview(final PointerTracker tracker) { - mDrawingHandler.dismissKeyPreview(mKeyPreviewLingerTimeout, tracker); + public void dismissKeyPreviewWithoutDelay(final Key key) { + mKeyPreviewChoreographer.dismissKeyPreview(key, false /* withAnimation */); + // To redraw key top letter. + invalidateKey(key); + } + + @Override + public void dismissKeyPreview(final Key key) { + if (!isHardwareAccelerated()) { + // TODO: Implement preference option to control key preview method and duration. + mDrawingHandler.dismissKeyPreview(mKeyPreviewDrawParams.getLingerTimeout(), key); + return; + } + mKeyPreviewChoreographer.dismissKeyPreview(key, true /* withAnimation */); } public void setSlidingKeyInputPreviewEnabled(final boolean enabled) { - mSlidingKeyInputPreview.setPreviewEnabled(enabled); + mSlidingKeyInputDrawingPreview.setPreviewEnabled(enabled); } @Override public void showSlidingKeyInputPreview(final PointerTracker tracker) { locatePreviewPlacerView(); - mSlidingKeyInputPreview.setPreviewPosition(tracker); + mSlidingKeyInputDrawingPreview.setPreviewPosition(tracker); } @Override public void dismissSlidingKeyInputPreview() { - mSlidingKeyInputPreview.dismissSlidingKeyInputPreview(); + mSlidingKeyInputDrawingPreview.dismissSlidingKeyInputPreview(); } private void setGesturePreviewMode(final boolean isGestureTrailEnabled, final boolean isGestureFloatingPreviewTextEnabled) { - mGestureFloatingPreviewText.setPreviewEnabled(isGestureFloatingPreviewTextEnabled); - mGestureTrailsPreview.setPreviewEnabled(isGestureTrailEnabled); + mGestureFloatingTextDrawingPreview.setPreviewEnabled(isGestureFloatingPreviewTextEnabled); + mGestureTrailsDrawingPreview.setPreviewEnabled(isGestureTrailEnabled); } + // Implements {@link DrawingHandler.Callbacks} method. + @Override public void showGestureFloatingPreviewText(final SuggestedWords suggestedWords) { locatePreviewPlacerView(); - mGestureFloatingPreviewText.setSuggetedWords(suggestedWords); + mGestureFloatingTextDrawingPreview.setSuggetedWords(suggestedWords); } public void dismissGestureFloatingPreviewText() { @@ -862,12 +511,13 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack final boolean showsFloatingPreviewText) { locatePreviewPlacerView(); if (showsFloatingPreviewText) { - mGestureFloatingPreviewText.setPreviewPosition(tracker); + mGestureFloatingTextDrawingPreview.setPreviewPosition(tracker); } - mGestureTrailsPreview.setPreviewPosition(tracker); + mGestureTrailsDrawingPreview.setPreviewPosition(tracker); } // Note that this method is called from a non-UI thread. + @SuppressWarnings("static-method") public void setMainDictionaryAvailability(final boolean mainDictionaryAvailable) { PointerTracker.setMainDictionaryAvailability(mainDictionaryAvailable); } @@ -883,34 +533,35 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); - // Notify the ResearchLogger (development only diagnostics) that the keyboard view has - // been attached. This is needed to properly show the splash screen, which requires that - // the window token of the KeyboardView be non-null. - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.getInstance().mainKeyboardView_onAttachedToWindow(this); - } + installPreviewPlacerView(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - mPreviewPlacerView.removeAllViews(); - // Notify the ResearchLogger (development only diagnostics) that the keyboard view has - // been detached. This is needed to invalidate the reference of {@link MainKeyboardView} - // to null. - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.getInstance().mainKeyboardView_onDetachedFromWindow(); - } + mDrawingPreviewPlacerView.removeAllViews(); } private MoreKeysPanel onCreateMoreKeysPanel(final Key key, final Context context) { - if (key.getMoreKeys() == null) { + final MoreKeySpec[] moreKeys = key.getMoreKeys(); + if (moreKeys == null) { return null; } Keyboard moreKeysKeyboard = mMoreKeysKeyboardCache.get(key); if (moreKeysKeyboard == null) { - moreKeysKeyboard = new MoreKeysKeyboard.Builder( - context, key, this, mKeyPreviewDrawParams).build(); + // {@link KeyPreviewDrawParams#mPreviewVisibleWidth} should have been set at + // {@link KeyPreviewChoreographer#placeKeyPreview(Key,TextView,KeyboardIconsSet,KeyDrawParams,int,int[]}, + // though there may be some chances that the value is zero. <code>width == 0</code> + // will cause zero-division error at + // {@link MoreKeysKeyboardParams#setParameters(int,int,int,int,int,int,boolean,int)}. + final boolean isSingleMoreKeyWithPreview = mKeyPreviewDrawParams.isPopupEnabled() + && !key.noKeyPreview() && moreKeys.length == 1 + && mKeyPreviewDrawParams.getVisibleWidth() > 0; + final MoreKeysKeyboard.Builder builder = new MoreKeysKeyboard.Builder( + context, key, getKeyboard(), isSingleMoreKeyWithPreview, + mKeyPreviewDrawParams.getVisibleWidth(), + mKeyPreviewDrawParams.getVisibleHeight(), newLabelPaint(key)); + moreKeysKeyboard = builder.build(); mMoreKeysKeyboardCache.put(key, moreKeysKeyboard); } @@ -922,11 +573,13 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack return moreKeysKeyboardView; } + // Implements {@link TimerHandler.Callbacks} method. /** * Called when a key is long pressed. * @param tracker the pointer tracker which pressed the parent key */ - private void onLongPress(final PointerTracker tracker) { + @Override + public void onLongPress(final PointerTracker tracker) { if (isShowingMoreKeysPanel()) { return; } @@ -934,16 +587,13 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack if (key == null) { return; } - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.mainKeyboardView_onLongPress(); - } final KeyboardActionListener listener = mKeyboardActionListener; if (key.hasNoPanelAutoMoreKey()) { final int moreKeyCode = key.getMoreKeys()[0].mCode; tracker.onLongPressed(); listener.onPressKey(moreKeyCode, 0 /* repeatCount */, true /* isSinglePointer */); - listener.onCodeInput(moreKeyCode, - Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); + listener.onCodeInput(moreKeyCode, Constants.NOT_A_COORDINATE, + Constants.NOT_A_COORDINATE, false /* isKeyRepeat */); listener.onReleaseKey(moreKeyCode, false /* withSliding */); return; } @@ -967,7 +617,8 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack final int[] lastCoords = CoordinateUtils.newInstance(); tracker.getLastCoordinates(lastCoords); - final boolean keyPreviewEnabled = isKeyPreviewPopupEnabled() && !key.noKeyPreview(); + final boolean keyPreviewEnabled = mKeyPreviewDrawParams.isPopupEnabled() + && !key.noKeyPreview(); // The more keys keyboard is usually horizontally aligned with the center of the parent key. // If showMoreKeysKeyboardAtTouchedPoint is true and the key preview is disabled, the more // keys keyboard is placed at the touch point of the parent key. @@ -979,26 +630,24 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack // aligned with the bottom edge of the visible part of the key preview. // {@code mPreviewVisibleOffset} has been set appropriately in // {@link KeyboardView#showKeyPreview(PointerTracker)}. - final int pointY = key.getY() + mKeyPreviewDrawParams.mPreviewVisibleOffset; + final int pointY = key.getY() + mKeyPreviewDrawParams.getVisibleOffset(); moreKeysPanel.showMoreKeysPanel(this, this, pointX, pointY, mKeyboardActionListener); tracker.onShowMoreKeysPanel(moreKeysPanel); + // TODO: Implement zoom in animation of more keys panel. + dismissKeyPreviewWithoutDelay(key); } - public boolean isInSlidingKeyInput() { + public boolean isInDraggingFinger() { if (isShowingMoreKeysPanel()) { return true; } - return PointerTracker.isAnyInSlidingKeyInput(); + return PointerTracker.isAnyInDraggingFinger(); } @Override public void onShowMoreKeysPanel(final MoreKeysPanel panel) { locatePreviewPlacerView(); - // TODO: Remove this check - if (panel.isShowingInParent()) { - panel.dismissMoreKeysPanel(); - } - mPreviewPlacerView.addView(panel.getContainerView()); + panel.showInParent(mDrawingPreviewPlacerView); mMoreKeysPanel = panel; dimEntireKeyboard(true /* dimmed */); } @@ -1008,15 +657,15 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack } @Override - public void onCancelMoreKeysPanel(final MoreKeysPanel panel) { + public void onCancelMoreKeysPanel() { PointerTracker.dismissAllMoreKeysPanels(); } @Override - public void onDismissMoreKeysPanel(final MoreKeysPanel panel) { + public void onDismissMoreKeysPanel() { dimEntireKeyboard(false /* dimmed */); if (isShowingMoreKeysPanel()) { - mPreviewPlacerView.removeView(mMoreKeysPanel.getContainerView()); + mMoreKeysPanel.removeFromParent(); mMoreKeysPanel = null; } } @@ -1034,14 +683,6 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack } @Override - public boolean dispatchTouchEvent(MotionEvent event) { - if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { - return AccessibleKeyboardViewProxy.getInstance().dispatchTouchEvent(event); - } - return super.dispatchTouchEvent(event); - } - - @Override public boolean onTouchEvent(final MotionEvent me) { if (getKeyboard() == null) { return false; @@ -1049,28 +690,26 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack if (mNonDistinctMultitouchHelper != null) { if (me.getPointerCount() > 1 && mKeyTimerHandler.isInKeyRepeat()) { // Key repeating timer will be canceled if 2 or more keys are in action. - mKeyTimerHandler.cancelKeyRepeatTimer(); + mKeyTimerHandler.cancelKeyRepeatTimers(); } // Non distinct multitouch screen support - mNonDistinctMultitouchHelper.processMotionEvent(me, this); + mNonDistinctMultitouchHelper.processMotionEvent(me, mKeyDetector); return true; } return processMotionEvent(me); } public boolean processMotionEvent(final MotionEvent me) { - if (LatinImeLogger.sUsabilityStudy) { - UsabilityStudyLogUtils.writeMotionEvent(me); - } - // Currently the same "move" event is being logged twice. - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.mainKeyboardView_processMotionEvent(me); - } - final int index = me.getActionIndex(); final int id = me.getPointerId(index); - final PointerTracker tracker = PointerTracker.getPointerTracker(id, this); - tracker.processMotionEvent(me, this); + final PointerTracker tracker = PointerTracker.getPointerTracker(id); + // When a more keys panel is showing, we should ignore other fingers' single touch events + // other than the finger that is showing the more keys panel. + if (isShowingMoreKeysPanel() && !tracker.isShowingMoreKeysPanel() + && PointerTracker.getActivePointerTrackerCount() == 1) { + return true; + } + tracker.processMotionEvent(me, mKeyDetector); return true; } @@ -1089,22 +728,24 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack mMoreKeysKeyboardCache.clear(); } + public void onHideWindow() { + onDismissMoreKeysPanel(); + final MainKeyboardAccessibilityDelegate accessibilityDelegate = mAccessibilityDelegate; + if (accessibilityDelegate != null) { + accessibilityDelegate.onHideWindow(); + } + } + /** - * Receives hover events from the input framework. - * - * @param event The motion event to be dispatched. - * @return {@code true} if the event was handled by the view, {@code false} - * otherwise + * {@inheritDoc} */ @Override - public boolean dispatchHoverEvent(final MotionEvent event) { - if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { - final PointerTracker tracker = PointerTracker.getPointerTracker(0, this); - return AccessibleKeyboardViewProxy.getInstance().dispatchHoverEvent(event, tracker); + public boolean onHoverEvent(final MotionEvent event) { + final MainKeyboardAccessibilityDelegate accessibilityDelegate = mAccessibilityDelegate; + if (accessibilityDelegate != null) { + return accessibilityDelegate.onHoverEvent(event); } - - // Reflection doesn't support calling superclass methods. - return false; + return super.onHoverEvent(event); } public void updateShortcutKey(final boolean available) { @@ -1121,14 +762,16 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack } public void startDisplayLanguageOnSpacebar(final boolean subtypeChanged, - final boolean needsToDisplayLanguage, final boolean hasMultipleEnabledIMEsOrSubtypes) { - mNeedsToDisplayLanguage = needsToDisplayLanguage; + final int languageOnSpacebarFormatType, + final boolean hasMultipleEnabledIMEsOrSubtypes) { + mLanguageOnSpacebarFormatType = languageOnSpacebarFormatType; mHasMultipleEnabledIMEsOrSubtypes = hasMultipleEnabledIMEsOrSubtypes; final ObjectAnimator animator = mLanguageOnSpacebarFadeoutAnimator; if (animator == null) { - mNeedsToDisplayLanguage = false; + mLanguageOnSpacebarFormatType = LanguageOnSpacebarHelper.FORMAT_TYPE_NONE; } else { - if (subtypeChanged && needsToDisplayLanguage) { + if (subtypeChanged + && languageOnSpacebarFormatType != LanguageOnSpacebarHelper.FORMAT_TYPE_NONE) { setLanguageOnSpacebarAnimAlpha(Constants.Color.ALPHA_OPAQUE); if (animator.isStarted()) { animator.cancel(); @@ -1143,14 +786,6 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack invalidateKey(mSpaceKey); } - public void updateAutoCorrectionState(final boolean isAutoCorrection) { - if (!mAutoCorrectionSpacebarLedEnabled) { - return; - } - mAutoCorrectionSpacebarLedOn = isAutoCorrection; - invalidateKey(mSpaceKey); - } - private void dimEntireKeyboard(final boolean dimmed) { final boolean needsRedrawing = mNeedsToDimEntireKeyboard != dimmed; mNeedsToDimEntireKeyboard = dimmed; @@ -1175,25 +810,26 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack if (key.altCodeWhileTyping() && key.isEnabled()) { params.mAnimAlpha = mAltCodeKeyWhileTypingAnimAlpha; } + super.onDrawKeyTopVisuals(key, canvas, paint, params); final int code = key.getCode(); if (code == Constants.CODE_SPACE) { - drawSpacebar(key, canvas, paint); + // If input language are explicitly selected. + if (mLanguageOnSpacebarFormatType != LanguageOnSpacebarHelper.FORMAT_TYPE_NONE) { + drawLanguageOnSpacebar(key, canvas, paint); + } // Whether space key needs to show the "..." popup hint for special purposes if (key.isLongPressEnabled() && mHasMultipleEnabledIMEsOrSubtypes) { drawKeyPopupHint(key, canvas, paint, params); } } else if (code == Constants.CODE_LANGUAGE_SWITCH) { - super.onDrawKeyTopVisuals(key, canvas, paint, params); drawKeyPopupHint(key, canvas, paint, params); - } else { - super.onDrawKeyTopVisuals(key, canvas, paint, params); } } private boolean fitsTextIntoWidth(final int width, final String text, final Paint paint) { final int maxTextWidth = width - mLanguageOnSpacebarHorizontalMargin * 2; paint.setTextScaleX(1.0f); - final float textWidth = TypefaceUtils.getLabelWidth(text, paint); + final float textWidth = TypefaceUtils.getStringWidth(text, paint); if (textWidth < width) { return true; } @@ -1204,74 +840,56 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack } paint.setTextScaleX(scaleX); - return TypefaceUtils.getLabelWidth(text, paint) < maxTextWidth; + return TypefaceUtils.getStringWidth(text, paint) < maxTextWidth; } // Layout language name on spacebar. private String layoutLanguageOnSpacebar(final Paint paint, final InputMethodSubtype subtype, final int width) { - // Choose appropriate language name to fit into the width. - final String fullText = SubtypeLocaleUtils.getFullDisplayName(subtype); - if (fitsTextIntoWidth(width, fullText, paint)) { - return fullText; + if (mLanguageOnSpacebarFormatType == LanguageOnSpacebarHelper.FORMAT_TYPE_FULL_LOCALE) { + final String fullText = SpacebarLanguageUtils.getFullDisplayName(subtype); + if (fitsTextIntoWidth(width, fullText, paint)) { + return fullText; + } } - final String middleText = SubtypeLocaleUtils.getMiddleDisplayName(subtype); + final String middleText = SpacebarLanguageUtils.getMiddleDisplayName(subtype); if (fitsTextIntoWidth(width, middleText, paint)) { return middleText; } - final String shortText = SubtypeLocaleUtils.getShortDisplayName(subtype); - if (fitsTextIntoWidth(width, shortText, paint)) { - return shortText; - } - return ""; } - private void drawSpacebar(final Key key, final Canvas canvas, final Paint paint) { + private void drawLanguageOnSpacebar(final Key key, final Canvas canvas, final Paint paint) { final int width = key.getWidth(); final int height = key.getHeight(); - - // If input language are explicitly selected. - if (mNeedsToDisplayLanguage) { - paint.setTextAlign(Align.CENTER); - paint.setTypeface(Typeface.DEFAULT); - paint.setTextSize(mSpacebarTextSize); - final InputMethodSubtype subtype = getKeyboard().mId.mSubtype; - final String language = layoutLanguageOnSpacebar(paint, subtype, width); - // Draw language text with shadow - final float descent = paint.descent(); - final float textHeight = -paint.ascent() + descent; - final float baseline = height / 2 + textHeight / 2; - paint.setColor(mSpacebarTextShadowColor); - paint.setAlpha(mLanguageOnSpacebarAnimAlpha); - canvas.drawText(language, width / 2, baseline - descent - 1, paint); - paint.setColor(mSpacebarTextColor); - paint.setAlpha(mLanguageOnSpacebarAnimAlpha); - canvas.drawText(language, width / 2, baseline - descent, paint); - } - - // Draw the spacebar icon at the bottom - if (mAutoCorrectionSpacebarLedOn) { - final int iconWidth = width * SPACE_LED_LENGTH_PERCENT / 100; - final int iconHeight = mAutoCorrectionSpacebarLedIcon.getIntrinsicHeight(); - int x = (width - iconWidth) / 2; - int y = height - iconHeight; - drawIcon(canvas, mAutoCorrectionSpacebarLedIcon, x, y, iconWidth, iconHeight); - } else if (mSpaceIcon != null) { - final int iconWidth = mSpaceIcon.getIntrinsicWidth(); - final int iconHeight = mSpaceIcon.getIntrinsicHeight(); - int x = (width - iconWidth) / 2; - int y = height - iconHeight; - drawIcon(canvas, mSpaceIcon, x, y, iconWidth, iconHeight); + paint.setTextAlign(Align.CENTER); + paint.setTypeface(Typeface.DEFAULT); + paint.setTextSize(mLanguageOnSpacebarTextSize); + final InputMethodSubtype subtype = getKeyboard().mId.mSubtype; + final String language = layoutLanguageOnSpacebar(paint, subtype, width); + // Draw language text with shadow + final float descent = paint.descent(); + final float textHeight = -paint.ascent() + descent; + final float baseline = height / 2 + textHeight / 2; + if (mLanguageOnSpacebarTextShadowRadius > 0.0f) { + paint.setShadowLayer(mLanguageOnSpacebarTextShadowRadius, 0, 0, + mLanguageOnSpacebarTextShadowColor); + } else { + paint.clearShadowLayer(); } + paint.setColor(mLanguageOnSpacebarTextColor); + paint.setAlpha(mLanguageOnSpacebarAnimAlpha); + canvas.drawText(language, width / 2, baseline - descent, paint); + paint.clearShadowLayer(); + paint.setTextScaleX(1.0f); } @Override public void deallocateMemory() { super.deallocateMemory(); - mGestureTrailsPreview.deallocateMemory(); + mDrawingPreviewPlacerView.deallocateMemory(); } } diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java b/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java index 6b76e2461..5a9d4755f 100644 --- a/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java +++ b/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java @@ -21,25 +21,29 @@ public final class MoreKeysDetector extends KeyDetector { private final int mSlideAllowanceSquareTop; public MoreKeysDetector(float slideAllowance) { - super(/* keyHysteresisDistance */0); + super(); mSlideAllowanceSquare = (int)(slideAllowance * slideAllowance); // Top slide allowance is slightly longer (sqrt(2) times) than other edges. mSlideAllowanceSquareTop = mSlideAllowanceSquare * 2; } @Override - public boolean alwaysAllowsSlidingInput() { + public boolean alwaysAllowsKeySelectionByDraggingFinger() { return true; } @Override - public Key detectHitKey(int x, int y) { + public Key detectHitKey(final int x, final int y) { + final Keyboard keyboard = getKeyboard(); + if (keyboard == null) { + return null; + } final int touchX = getTouchX(x); final int touchY = getTouchY(y); Key nearestKey = null; int nearestDist = (y < 0) ? mSlideAllowanceSquareTop : mSlideAllowanceSquare; - for (final Key key : getKeyboard().getKeys()) { + for (final Key key : keyboard.getSortedKeys()) { final int dist = key.squaredDistanceToEdge(touchX, touchY); if (dist < nearestDist) { nearestKey = key; diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java index 8256d4623..e0184d7f1 100644 --- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java +++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java @@ -21,7 +21,6 @@ import android.graphics.Paint; import android.graphics.drawable.Drawable; import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.keyboard.internal.KeyPreviewDrawParams; import com.android.inputmethod.keyboard.internal.KeyboardBuilder; import com.android.inputmethod.keyboard.internal.KeyboardIconsSet; import com.android.inputmethod.keyboard.internal.KeyboardParams; @@ -223,7 +222,7 @@ public final class MoreKeysKeyboard extends Keyboard { } public int getDefaultKeyCoordX() { - return mLeftKeys * mColumnWidth; + return mLeftKeys * mColumnWidth + mLeftPadding; } public int getX(final int n, final int row) { @@ -260,33 +259,27 @@ public final class MoreKeysKeyboard extends Keyboard { /** * The builder of MoreKeysKeyboard. * @param context the context of {@link MoreKeysKeyboardView}. - * @param parentKey the {@link Key} that invokes more keys keyboard. - * @param parentKeyboardView the {@link KeyboardView} that contains the parentKey. + * @param key the {@link Key} that invokes more keys keyboard. + * @param keyboard the {@link Keyboard} that contains the parentKey. + * @param isSingleMoreKeyWithPreview true if the <code>key</code> has just a single + * "more key" and its key popup preview is enabled. * @param keyPreviewDrawParams the parameter to place key preview. + * @param paintToMeasure the {@link Paint} object to measure a "more key" width */ - public Builder(final Context context, final Key parentKey, - final MainKeyboardView parentKeyboardView, - final KeyPreviewDrawParams keyPreviewDrawParams) { + public Builder(final Context context, final Key key, final Keyboard keyboard, + final boolean isSingleMoreKeyWithPreview, final int keyPreviewVisibleWidth, + final int keyPreviewVisibleHeight, final Paint paintToMeasure) { super(context, new MoreKeysKeyboardParams()); - final Keyboard parentKeyboard = parentKeyboardView.getKeyboard(); - load(parentKeyboard.mMoreKeysTemplate, parentKeyboard.mId); + load(keyboard.mMoreKeysTemplate, keyboard.mId); // TODO: More keys keyboard's vertical gap is currently calculated heuristically. // Should revise the algorithm. - mParams.mVerticalGap = parentKeyboard.mVerticalGap / 2; - mParentKey = parentKey; - - final MoreKeySpec[] moreKeys = parentKey.getMoreKeys(); - final int width, height; - // {@link KeyPreviewDrawParams#mPreviewVisibleWidth} should have been set at - // {@link MainKeyboardView#showKeyPreview(PointerTracker}, though there may be - // some chances that the value is zero. <code>width == 0</code> will cause - // zero-division error at - // {@link MoreKeysKeyboardParams#setParameters(int,int,int,int,int,int,boolean,int)}. - final boolean singleMoreKeyWithPreview = parentKeyboardView.isKeyPreviewPopupEnabled() - && !parentKey.noKeyPreview() && moreKeys.length == 1 - && keyPreviewDrawParams.mPreviewVisibleWidth > 0; - if (singleMoreKeyWithPreview) { + mParams.mVerticalGap = keyboard.mVerticalGap / 2; + // This {@link MoreKeysKeyboard} is invoked from the <code>key</code>. + mParentKey = key; + + final int keyWidth, rowHeight; + if (isSingleMoreKeyWithPreview) { // Use pre-computed width and height if this more keys keyboard has only one key to // mitigate visual flicker between key preview and more keys keyboard. // Caveats for the visual assets: To achieve this effect, both the key preview @@ -294,29 +287,28 @@ public final class MoreKeysKeyboard extends Keyboard { // left/right/top paddings. The bottom paddings of both backgrounds don't need to // be considered because the vertical positions of both backgrounds were already // adjusted with their bottom paddings deducted. - width = keyPreviewDrawParams.mPreviewVisibleWidth; - height = keyPreviewDrawParams.mPreviewVisibleHeight + mParams.mVerticalGap; + keyWidth = keyPreviewVisibleWidth; + rowHeight = keyPreviewVisibleHeight + mParams.mVerticalGap; } else { final float padding = context.getResources().getDimension( - R.dimen.more_keys_keyboard_key_horizontal_padding) - + (parentKey.hasLabelsInMoreKeys() + R.dimen.config_more_keys_keyboard_key_horizontal_padding) + + (key.hasLabelsInMoreKeys() ? mParams.mDefaultKeyWidth * LABEL_PADDING_RATIO : 0.0f); - width = getMaxKeyWidth(parentKey, mParams.mDefaultKeyWidth, padding, - parentKeyboardView.newLabelPaint(parentKey)); - height = parentKeyboard.mMostCommonKeyHeight; + keyWidth = getMaxKeyWidth(key, mParams.mDefaultKeyWidth, padding, paintToMeasure); + rowHeight = keyboard.mMostCommonKeyHeight; } final int dividerWidth; - if (parentKey.needsDividersInMoreKeys()) { + if (key.needsDividersInMoreKeys()) { mDivider = mResources.getDrawable(R.drawable.more_keys_divider); - dividerWidth = (int)(width * DIVIDER_RATIO); + dividerWidth = (int)(keyWidth * DIVIDER_RATIO); } else { mDivider = null; dividerWidth = 0; } - mParams.setParameters(moreKeys.length, parentKey.getMoreKeysColumn(), - width, height, parentKey.getX() + parentKey.getWidth() / 2, - parentKeyboard.mId.mWidth, parentKey.isFixedColumnOrderMoreKeys(), - dividerWidth); + final MoreKeySpec[] moreKeys = key.getMoreKeys(); + mParams.setParameters(moreKeys.length, key.getMoreKeysColumn(), keyWidth, rowHeight, + key.getX() + key.getWidth() / 2, keyboard.mId.mWidth, + key.isFixedColumnOrderMoreKeys(), dividerWidth); } private static int getMaxKeyWidth(final Key parentKey, final int minKeyWidth, @@ -327,7 +319,7 @@ public final class MoreKeysKeyboard extends Keyboard { // If the label is single letter, minKeyWidth is enough to hold the label. if (label != null && StringUtils.codePointCount(label) > 1) { maxWidth = Math.max(maxWidth, - (int)(TypefaceUtils.getLabelWidth(label, paint) + padding)); + (int)(TypefaceUtils.getStringWidth(label, paint) + padding)); } } return maxWidth; @@ -343,8 +335,7 @@ public final class MoreKeysKeyboard extends Keyboard { final int row = n / params.mNumColumns; final int x = params.getX(n, row); final int y = params.getY(row); - final Key key = new Key(params, moreKeySpec, x, y, - params.mDefaultKeyWidth, params.mDefaultRowHeight, moreKeyFlags); + final Key key = moreKeySpec.buildKey(x, y, moreKeyFlags, params); params.markAsEdgeKey(key, row); params.onAddKey(key); diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java index 973128d36..5140c4ffc 100644 --- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java @@ -17,11 +17,13 @@ package com.android.inputmethod.keyboard; import android.content.Context; -import android.content.res.Resources; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; +import com.android.inputmethod.accessibility.AccessibilityUtils; +import com.android.inputmethod.accessibility.MoreKeysKeyboardAccessibilityDelegate; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.utils.CoordinateUtils; @@ -42,6 +44,8 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel private int mActivePointerId; + protected MoreKeysKeyboardAccessibilityDelegate mAccessibilityDelegate; + public MoreKeysKeyboardView(final Context context, final AttributeSet attrs) { this(context, attrs, R.attr.moreKeysKeyboardViewStyle); } @@ -49,10 +53,8 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel public MoreKeysKeyboardView(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); - - final Resources res = context.getResources(); - mKeyDetector = new MoreKeysDetector( - res.getDimension(R.dimen.more_keys_keyboard_slide_allowance)); + mKeyDetector = new MoreKeysDetector(getResources().getDimension( + R.dimen.config_more_keys_keyboard_slide_allowance)); } @Override @@ -70,8 +72,19 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel @Override public void setKeyboard(final Keyboard keyboard) { super.setKeyboard(keyboard); - mKeyDetector.setKeyboard(keyboard, -getPaddingLeft(), - -getPaddingTop() + getVerticalCorrection()); + mKeyDetector.setKeyboard( + keyboard, -getPaddingLeft(), -getPaddingTop() + getVerticalCorrection()); + if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { + if (mAccessibilityDelegate == null) { + mAccessibilityDelegate = new MoreKeysKeyboardAccessibilityDelegate( + this, mKeyDetector); + mAccessibilityDelegate.setOpenAnnounce(R.string.spoken_open_more_keys_keyboard); + mAccessibilityDelegate.setCloseAnnounce(R.string.spoken_close_more_keys_keyboard); + } + mAccessibilityDelegate.setKeyboard(keyboard); + } else { + mAccessibilityDelegate = null; + } } @Override @@ -81,11 +94,13 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel mListener = listener; final View container = getContainerView(); // The coordinates of panel's left-top corner in parentView's coordinate system. - final int x = pointX - getDefaultCoordX() - container.getPaddingLeft(); - final int y = pointY - container.getMeasuredHeight() + container.getPaddingBottom(); + // We need to consider background drawable paddings. + final int x = pointX - getDefaultCoordX() - container.getPaddingLeft() - getPaddingLeft(); + final int y = pointY - container.getMeasuredHeight() + container.getPaddingBottom() + + getPaddingBottom(); parentView.getLocationInWindow(mCoordinates); - // Ensure the horizontal position of the panel does not extend past the screen edges. + // Ensure the horizontal position of the panel does not extend past the parentView edges. final int maxX = parentView.getMeasuredWidth() - container.getMeasuredWidth(); final int panelX = Math.max(0, Math.min(maxX, x)) + CoordinateUtils.x(mCoordinates); final int panelY = y + CoordinateUtils.y(mCoordinates); @@ -95,6 +110,10 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel mOriginX = x + container.getPaddingLeft(); mOriginY = y + container.getPaddingTop(); controller.onShowMoreKeysPanel(this); + final MoreKeysKeyboardAccessibilityDelegate accessibilityDelegate = mAccessibilityDelegate; + if (accessibilityDelegate != null) { + accessibilityDelegate.onShowMoreKeysKeyboard(); + } } /** @@ -107,27 +126,33 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel @Override public void onDownEvent(final int x, final int y, final int pointerId, final long eventTime) { mActivePointerId = pointerId; - onMoveKeyInternal(x, y, pointerId); + mCurrentKey = detectKey(x, y); } @Override - public void onMoveEvent(int x, int y, final int pointerId, long eventTime) { + public void onMoveEvent(final int x, final int y, final int pointerId, final long eventTime) { if (mActivePointerId != pointerId) { return; } final boolean hasOldKey = (mCurrentKey != null); - onMoveKeyInternal(x, y, pointerId); + mCurrentKey = detectKey(x, y); if (hasOldKey && mCurrentKey == null) { - // If the pointer has moved too far away from any target then cancel the panel. - mController.onCancelMoreKeysPanel(this); + // A more keys keyboard is canceled when detecting no key. + mController.onCancelMoreKeysPanel(); } } @Override public void onUpEvent(final int x, final int y, final int pointerId, final long eventTime) { - if (mCurrentKey != null && mActivePointerId == pointerId) { + if (mActivePointerId != pointerId) { + return; + } + // Calling {@link #detectKey(int,int,int)} here is harmless because the last move event and + // the following up event share the same coordinates. + mCurrentKey = detectKey(x, y); + if (mCurrentKey != null) { updateReleaseKeyGraphics(mCurrentKey); - onCodeInput(mCurrentKey.getCode(), x, y); + onKeyInput(mCurrentKey, x, y); mCurrentKey = null; } } @@ -135,31 +160,36 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel /** * Performs the specific action for this panel when the user presses a key on the panel. */ - protected void onCodeInput(final int code, final int x, final int y) { + protected void onKeyInput(final Key key, final int x, final int y) { + final int code = key.getCode(); if (code == Constants.CODE_OUTPUT_TEXT) { mListener.onTextInput(mCurrentKey.getOutputText()); } else if (code != Constants.CODE_UNSPECIFIED) { - mListener.onCodeInput(code, x, y); + if (getKeyboard().hasProximityCharsCorrection(code)) { + mListener.onCodeInput(code, x, y, false /* isKeyRepeat */); + } else { + mListener.onCodeInput(code, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, + false /* isKeyRepeat */); + } } } - private void onMoveKeyInternal(int x, int y, int pointerId) { - if (mActivePointerId != pointerId) { - // Ignore old pointers when newer pointer is active. - return; - } + private Key detectKey(int x, int y) { final Key oldKey = mCurrentKey; final Key newKey = mKeyDetector.detectHitKey(x, y); - if (newKey != oldKey) { - mCurrentKey = newKey; - invalidateKey(mCurrentKey); - if (oldKey != null) { - updateReleaseKeyGraphics(oldKey); - } - if (newKey != null) { - updatePressKeyGraphics(newKey); - } + if (newKey == oldKey) { + return newKey; } + // A new key is detected. + if (oldKey != null) { + updateReleaseKeyGraphics(oldKey); + invalidateKey(oldKey); + } + if (newKey != null) { + updatePressKeyGraphics(newKey); + invalidateKey(newKey); + } + return newKey; } private void updateReleaseKeyGraphics(final Key key) { @@ -177,7 +207,11 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel if (!isShowingInParent()) { return; } - mController.onDismissMoreKeysPanel(this); + final MoreKeysKeyboardAccessibilityDelegate accessibilityDelegate = mAccessibilityDelegate; + if (accessibilityDelegate != null) { + accessibilityDelegate.onDismissMoreKeysKeyboard(); + } + mController.onDismissMoreKeysPanel(); } @Override @@ -214,12 +248,38 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel return true; } + /** + * {@inheritDoc} + */ @Override - public View getContainerView() { + public boolean onHoverEvent(final MotionEvent event) { + final MoreKeysKeyboardAccessibilityDelegate accessibilityDelegate = mAccessibilityDelegate; + if (accessibilityDelegate != null) { + return accessibilityDelegate.onHoverEvent(event); + } + return super.onHoverEvent(event); + } + + private View getContainerView() { return (View)getParent(); } @Override + public void showInParent(final ViewGroup parentView) { + removeFromParent(); + parentView.addView(getContainerView()); + } + + @Override + public void removeFromParent() { + final View containerView = getContainerView(); + final ViewGroup currentParent = (ViewGroup)containerView.getParent(); + if (currentParent != null) { + currentParent.removeView(containerView); + } + } + + @Override public boolean isShowingInParent() { return (getContainerView().getParent() != null); } diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysPanel.java b/java/src/com/android/inputmethod/keyboard/MoreKeysPanel.java index 886c6286f..7bddd09f6 100644 --- a/java/src/com/android/inputmethod/keyboard/MoreKeysPanel.java +++ b/java/src/com/android/inputmethod/keyboard/MoreKeysPanel.java @@ -17,6 +17,7 @@ package com.android.inputmethod.keyboard; import android.view.View; +import android.view.ViewGroup; public interface MoreKeysPanel { public interface Controller { @@ -28,24 +29,22 @@ public interface MoreKeysPanel { /** * Remove the current {@link MoreKeysPanel} from the target view. - * @param panel the panel to be dismissed. */ - public void onDismissMoreKeysPanel(final MoreKeysPanel panel); + public void onDismissMoreKeysPanel(); /** * Instructs the parent to cancel the panel (e.g., when entering a different input mode). - * @param panel the panel to be canceled. */ - public void onCancelMoreKeysPanel(final MoreKeysPanel panel); + public void onCancelMoreKeysPanel(); } public static final Controller EMPTY_CONTROLLER = new Controller() { @Override public void onShowMoreKeysPanel(final MoreKeysPanel panel) {} @Override - public void onDismissMoreKeysPanel(final MoreKeysPanel panel) {} + public void onDismissMoreKeysPanel() {} @Override - public void onCancelMoreKeysPanel(final MoreKeysPanel panel) {} + public void onCancelMoreKeysPanel() {} }; /** @@ -119,9 +118,16 @@ public interface MoreKeysPanel { public int translateY(int y); /** - * Return the view containing the more keys panel. + * Show this {@link MoreKeysPanel} in the parent view. + * + * @param parentView the {@link ViewGroup} that hosts this {@link MoreKeysPanel}. + */ + public void showInParent(ViewGroup parentView); + + /** + * Remove this {@link MoreKeysPanel} from the parent view. */ - public View getContainerView(); + public void removeFromParent(); /** * Return whether the panel is currently being shown. diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java index 52f190e77..b6905bc1c 100644 --- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java +++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java @@ -19,73 +19,40 @@ package com.android.inputmethod.keyboard; import android.content.res.Resources; import android.content.res.TypedArray; import android.os.SystemClock; -import android.util.DisplayMetrics; import android.util.Log; import android.view.MotionEvent; -import com.android.inputmethod.accessibility.AccessibilityUtils; -import com.android.inputmethod.keyboard.internal.GestureStroke; -import com.android.inputmethod.keyboard.internal.GestureStroke.GestureStrokeParams; -import com.android.inputmethod.keyboard.internal.GestureStrokeWithPreviewPoints; -import com.android.inputmethod.keyboard.internal.GestureStrokeWithPreviewPoints.GestureStrokePreviewParams; +import com.android.inputmethod.keyboard.internal.BatchInputArbiter; +import com.android.inputmethod.keyboard.internal.BatchInputArbiter.BatchInputArbiterListener; +import com.android.inputmethod.keyboard.internal.BogusMoveEventDetector; +import com.android.inputmethod.keyboard.internal.GestureEnabler; +import com.android.inputmethod.keyboard.internal.GestureStrokeDrawingParams; +import com.android.inputmethod.keyboard.internal.GestureStrokeDrawingPoints; +import com.android.inputmethod.keyboard.internal.GestureStrokeRecognitionParams; import com.android.inputmethod.keyboard.internal.PointerTrackerQueue; +import com.android.inputmethod.keyboard.internal.TypingTimeRecorder; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.InputPointers; import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.define.ProductionFlag; import com.android.inputmethod.latin.settings.Settings; -import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.CoordinateUtils; import com.android.inputmethod.latin.utils.ResourceUtils; -import com.android.inputmethod.research.ResearchLogger; import java.util.ArrayList; -public final class PointerTracker implements PointerTrackerQueue.Element { +public final class PointerTracker implements PointerTrackerQueue.Element, + BatchInputArbiterListener { private static final String TAG = PointerTracker.class.getSimpleName(); private static final boolean DEBUG_EVENT = false; private static final boolean DEBUG_MOVE_EVENT = false; private static final boolean DEBUG_LISTENER = false; private static boolean DEBUG_MODE = LatinImeLogger.sDBG || DEBUG_EVENT; - /** True if {@link PointerTracker}s should handle gesture events. */ - private static boolean sShouldHandleGesture = false; - private static boolean sMainDictionaryAvailable = false; - private static boolean sGestureHandlingEnabledByInputField = false; - private static boolean sGestureHandlingEnabledByUser = false; - - public interface KeyEventHandler { - /** - * Get KeyDetector object that is used for this PointerTracker. - * @return the KeyDetector object that is used for this PointerTracker - */ - public KeyDetector getKeyDetector(); - - /** - * Get KeyboardActionListener object that is used to register key code and so on. - * @return the KeyboardActionListner for this PointerTracke - */ - public KeyboardActionListener getKeyboardActionListener(); - - /** - * Get DrawingProxy object that is used for this PointerTracker. - * @return the DrawingProxy object that is used for this PointerTracker - */ - public DrawingProxy getDrawingProxy(); - - /** - * Get TimerProxy object that handles key repeat and long press timer event for this - * PointerTracker. - * @return the TimerProxy object that handles key repeat and long press timer event. - */ - public TimerProxy getTimerProxy(); - } - public interface DrawingProxy { public void invalidateKey(Key key); - public void showKeyPreview(PointerTracker tracker); - public void dismissKeyPreview(PointerTracker tracker); + public void showKeyPreview(Key key); + public void dismissKeyPreview(Key key); public void showSlidingKeyInputPreview(PointerTracker tracker); public void dismissSlidingKeyInputPreview(); public void showGestureTrail(PointerTracker tracker, boolean showsFloatingPreviewText); @@ -94,13 +61,14 @@ public final class PointerTracker implements PointerTrackerQueue.Element { public interface TimerProxy { public void startTypingStateTimer(Key typedKey); public boolean isTypingState(); - public void startKeyRepeatTimer(PointerTracker tracker, int repeatCount, int delay); - public void startLongPressTimer(PointerTracker tracker, int delay); - public void cancelLongPressTimer(); + public void startKeyRepeatTimerOf(PointerTracker tracker, int repeatCount, int delay); + public void startLongPressTimerOf(PointerTracker tracker, int delay); + public void cancelLongPressTimerOf(PointerTracker tracker); + public void cancelLongPressShiftKeyTimers(); + public void cancelKeyTimersOf(PointerTracker tracker); public void startDoubleTapShiftKeyTimer(); public void cancelDoubleTapShiftKeyTimer(); public boolean isInDoubleTapShiftKeyTimeout(); - public void cancelKeyTimers(); public void startUpdateBatchInputTimer(PointerTracker tracker); public void cancelUpdateBatchInputTimer(PointerTracker tracker); public void cancelAllUpdateBatchInputTimers(); @@ -111,11 +79,15 @@ public final class PointerTracker implements PointerTrackerQueue.Element { @Override public boolean isTypingState() { return false; } @Override - public void startKeyRepeatTimer(PointerTracker tracker, int repeatCount, int delay) {} + public void startKeyRepeatTimerOf(PointerTracker tracker, int repeatCount, int delay) {} + @Override + public void startLongPressTimerOf(PointerTracker tracker, int delay) {} @Override - public void startLongPressTimer(PointerTracker tracker, int delay) {} + public void cancelLongPressTimerOf(PointerTracker tracker) {} @Override - public void cancelLongPressTimer() {} + public void cancelLongPressShiftKeyTimers() {} + @Override + public void cancelKeyTimersOf(PointerTracker tracker) {} @Override public void startDoubleTapShiftKeyTimer() {} @Override @@ -123,8 +95,6 @@ public final class PointerTracker implements PointerTrackerQueue.Element { @Override public boolean isInDoubleTapShiftKeyTimeout() { return false; } @Override - public void cancelKeyTimers() {} - @Override public void startUpdateBatchInputTimer(PointerTracker tracker) {} @Override public void cancelUpdateBatchInputTimer(PointerTracker tracker) {} @@ -134,7 +104,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } static final class PointerTrackerParams { - public final boolean mSlidingKeyInputEnabled; + public final boolean mKeySelectionByDraggingFinger; public final int mTouchNoiseThresholdTime; public final int mTouchNoiseThresholdDistance; public final int mSuppressKeyPreviewAfterBatchInputDuration; @@ -142,21 +112,9 @@ public final class PointerTracker implements PointerTrackerQueue.Element { public final int mKeyRepeatInterval; public final int mLongPressShiftLockTimeout; - public static final PointerTrackerParams DEFAULT = new PointerTrackerParams(); - - private PointerTrackerParams() { - mSlidingKeyInputEnabled = false; - mTouchNoiseThresholdTime = 0; - mTouchNoiseThresholdDistance = 0; - mSuppressKeyPreviewAfterBatchInputDuration = 0; - mKeyRepeatStartTimeout = 0; - mKeyRepeatInterval = 0; - mLongPressShiftLockTimeout = 0; - } - public PointerTrackerParams(final TypedArray mainKeyboardViewAttr) { - mSlidingKeyInputEnabled = mainKeyboardViewAttr.getBoolean( - R.styleable.MainKeyboardView_slidingKeyInputEnable, false); + mKeySelectionByDraggingFinger = mainKeyboardViewAttr.getBoolean( + R.styleable.MainKeyboardView_keySelectionByDraggingFinger, false); mTouchNoiseThresholdTime = mainKeyboardViewAttr.getInt( R.styleable.MainKeyboardView_touchNoiseThresholdTime, 0); mTouchNoiseThresholdDistance = mainKeyboardViewAttr.getDimensionPixelSize( @@ -172,149 +130,36 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } } + private static GestureEnabler sGestureEnabler = new GestureEnabler(); + // Parameters for pointer handling. private static PointerTrackerParams sParams; - private static GestureStrokeParams sGestureStrokeParams; - private static GestureStrokePreviewParams sGesturePreviewParams; + private static GestureStrokeRecognitionParams sGestureStrokeRecognitionParams; + private static GestureStrokeDrawingParams sGestureStrokeDrawingParams; private static boolean sNeedsPhantomSuddenMoveEventHack; // Move this threshold to resource. // TODO: Device specific parameter would be better for device specific hack? private static final float PHANTOM_SUDDEN_MOVE_THRESHOLD = 0.25f; // in keyWidth - // This hack is applied to certain classes of tablets. - // See {@link #needsProximateBogusDownMoveUpEventHack(Resources)}. - private static boolean sNeedsProximateBogusDownMoveUpEventHack; - private static final ArrayList<PointerTracker> sTrackers = CollectionUtils.newArrayList(); + private static final ArrayList<PointerTracker> sTrackers = new ArrayList<>(); private static final PointerTrackerQueue sPointerTrackerQueue = new PointerTrackerQueue(); public final int mPointerId; - private DrawingProxy mDrawingProxy; - private TimerProxy mTimerProxy; - private KeyDetector mKeyDetector; - private KeyboardActionListener mListener = KeyboardActionListener.EMPTY_LISTENER; + private static DrawingProxy sDrawingProxy; + private static TimerProxy sTimerProxy; + private static KeyboardActionListener sListener = KeyboardActionListener.EMPTY_LISTENER; + // The {@link KeyDetector} is set whenever the down event is processed. Also this is updated + // when new {@link Keyboard} is set by {@link #setKeyDetector(KeyDetector)}. + private KeyDetector mKeyDetector = new KeyDetector(); private Keyboard mKeyboard; - private int mPhantonSuddenMoveThreshold; + private int mPhantomSuddenMoveThreshold; private final BogusMoveEventDetector mBogusMoveEventDetector = new BogusMoveEventDetector(); private boolean mIsDetectingGesture = false; // per PointerTracker. private static boolean sInGesture = false; - private static long sGestureFirstDownTime; - private static TimeRecorder sTimeRecorder; - private static final InputPointers sAggregratedPointers = new InputPointers( - GestureStroke.DEFAULT_CAPACITY); - private static int sLastRecognitionPointSize = 0; // synchronized using sAggregratedPointers - private static long sLastRecognitionTime = 0; // synchronized using sAggregratedPointers - - static final class BogusMoveEventDetector { - // Move these thresholds to resource. - // These thresholds' unit is a diagonal length of a key. - private static final float BOGUS_MOVE_ACCUMULATED_DISTANCE_THRESHOLD = 0.53f; - private static final float BOGUS_MOVE_RADIUS_THRESHOLD = 1.14f; - - private int mAccumulatedDistanceThreshold; - private int mRadiusThreshold; - - // Accumulated distance from actual and artificial down keys. - /* package */ int mAccumulatedDistanceFromDownKey; - private int mActualDownX; - private int mActualDownY; - - public void setKeyboardGeometry(final int keyWidth, final int keyHeight) { - final float keyDiagonal = (float)Math.hypot(keyWidth, keyHeight); - mAccumulatedDistanceThreshold = (int)( - keyDiagonal * BOGUS_MOVE_ACCUMULATED_DISTANCE_THRESHOLD); - mRadiusThreshold = (int)(keyDiagonal * BOGUS_MOVE_RADIUS_THRESHOLD); - } - - public void onActualDownEvent(final int x, final int y) { - mActualDownX = x; - mActualDownY = y; - } - - public void onDownKey() { - mAccumulatedDistanceFromDownKey = 0; - } - - public void onMoveKey(final int distance) { - mAccumulatedDistanceFromDownKey += distance; - } - - public boolean hasTraveledLongDistance(final int x, final int y) { - final int dx = Math.abs(x - mActualDownX); - final int dy = Math.abs(y - mActualDownY); - // A bogus move event should be a horizontal movement. A vertical movement might be - // a sloppy typing and should be ignored. - return dx >= dy && mAccumulatedDistanceFromDownKey >= mAccumulatedDistanceThreshold; - } - - /* package */ int getDistanceFromDownEvent(final int x, final int y) { - return getDistance(x, y, mActualDownX, mActualDownY); - } - - public boolean isCloseToActualDownEvent(final int x, final int y) { - return getDistanceFromDownEvent(x, y) < mRadiusThreshold; - } - } - - static final class TimeRecorder { - private final int mSuppressKeyPreviewAfterBatchInputDuration; - private final int mStaticTimeThresholdAfterFastTyping; // msec - private long mLastTypingTime; - private long mLastLetterTypingTime; - private long mLastBatchInputTime; - - public TimeRecorder(final PointerTrackerParams pointerTrackerParams, - final GestureStrokeParams gestureStrokeParams) { - mSuppressKeyPreviewAfterBatchInputDuration = - pointerTrackerParams.mSuppressKeyPreviewAfterBatchInputDuration; - mStaticTimeThresholdAfterFastTyping = - gestureStrokeParams.mStaticTimeThresholdAfterFastTyping; - } - - public boolean isInFastTyping(final long eventTime) { - final long elapsedTimeSinceLastLetterTyping = eventTime - mLastLetterTypingTime; - return elapsedTimeSinceLastLetterTyping < mStaticTimeThresholdAfterFastTyping; - } - - private boolean wasLastInputTyping() { - return mLastTypingTime >= mLastBatchInputTime; - } - - public void onCodeInput(final int code, final long eventTime) { - // Record the letter typing time when - // 1. Letter keys are typed successively without any batch input in between. - // 2. A letter key is typed within the threshold time since the last any key typing. - // 3. A non-letter key is typed within the threshold time since the last letter key - // typing. - if (Character.isLetter(code)) { - if (wasLastInputTyping() - || eventTime - mLastTypingTime < mStaticTimeThresholdAfterFastTyping) { - mLastLetterTypingTime = eventTime; - } - } else { - if (eventTime - mLastLetterTypingTime < mStaticTimeThresholdAfterFastTyping) { - // This non-letter typing should be treated as a part of fast typing. - mLastLetterTypingTime = eventTime; - } - } - mLastTypingTime = eventTime; - } - - public void onEndBatchInput(final long eventTime) { - mLastBatchInputTime = eventTime; - } - - public long getLastLetterTypingTime() { - return mLastLetterTypingTime; - } - - public boolean needsToSuppressKeyPreviewPopup(final long eventTime) { - return !wasLastInputTyping() - && eventTime - mLastBatchInputTime < mSuppressKeyPreviewAfterBatchInputDuration; - } - } + private static TypingTimeRecorder sTypingTimeRecorder; // The position and time at which first down event occurred. private long mDownTime; @@ -341,92 +186,63 @@ public final class PointerTracker implements PointerTrackerQueue.Element { private MoreKeysPanel mMoreKeysPanel; private static final int MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT = 3; - // true if this pointer is in a sliding key input. - boolean mIsInSlidingKeyInput; - // true if this pointer is in a sliding key input from a modifier key, + // true if this pointer is in the dragging finger mode. + boolean mIsInDraggingFinger; + // true if this pointer is sliding from a modifier key and in the sliding key input mode, // so that further modifier keys should be ignored. - boolean mIsInSlidingKeyInputFromModifier; + boolean mIsInSlidingKeyInput; // if not a NOT_A_CODE, the key of this code is repeating private int mCurrentRepeatingKeyCode = Constants.NOT_A_CODE; - // true if a sliding key input is allowed. - private boolean mIsAllowedSlidingKeyInput; - - private final GestureStrokeWithPreviewPoints mGestureStrokeWithPreviewPoints; - - private static final int SMALL_TABLET_SMALLEST_WIDTH = 600; // dp - private static final int LARGE_TABLET_SMALLEST_WIDTH = 768; // dp - - private static boolean needsProximateBogusDownMoveUpEventHack(final Resources res) { - // The proximate bogus down move up event hack is needed for a device such like, - // 1) is large tablet, or 2) is small tablet and the screen density is less than hdpi. - // Though it seems odd to use screen density as criteria of the quality of the touch - // screen, the small table that has a less density screen than hdpi most likely has been - // made with the touch screen that needs the hack. - final int sw = res.getConfiguration().smallestScreenWidthDp; - final boolean isLargeTablet = (sw >= LARGE_TABLET_SMALLEST_WIDTH); - final boolean isSmallTablet = - (sw >= SMALL_TABLET_SMALLEST_WIDTH && sw < LARGE_TABLET_SMALLEST_WIDTH); - final int densityDpi = res.getDisplayMetrics().densityDpi; - final boolean hasLowDensityScreen = (densityDpi < DisplayMetrics.DENSITY_HIGH); - final boolean needsTheHack = isLargeTablet || (isSmallTablet && hasLowDensityScreen); - if (DEBUG_MODE) { - Log.d(TAG, "needsProximateBogusDownMoveUpEventHack=" + needsTheHack - + " smallestScreenWidthDp=" + sw + " densityDpi=" + densityDpi); - } - return needsTheHack; - } + // true if dragging finger is allowed. + private boolean mIsAllowedDraggingFinger; - public static void init(final Resources res) { - sNeedsPhantomSuddenMoveEventHack = Boolean.parseBoolean( - ResourceUtils.getDeviceOverrideValue( - res, R.array.phantom_sudden_move_event_device_list)); - sNeedsProximateBogusDownMoveUpEventHack = needsProximateBogusDownMoveUpEventHack(res); - sParams = PointerTrackerParams.DEFAULT; - sGestureStrokeParams = GestureStrokeParams.DEFAULT; - sGesturePreviewParams = GestureStrokePreviewParams.DEFAULT; - sTimeRecorder = new TimeRecorder(sParams, sGestureStrokeParams); - } + private final BatchInputArbiter mBatchInputArbiter; + private final GestureStrokeDrawingPoints mGestureStrokeDrawingPoints; - public static void setParameters(final TypedArray mainKeyboardViewAttr) { + // TODO: Add PointerTrackerFactory singleton and move some class static methods into it. + public static void init(final TypedArray mainKeyboardViewAttr, final TimerProxy timerProxy, + final DrawingProxy drawingProxy) { sParams = new PointerTrackerParams(mainKeyboardViewAttr); - sGestureStrokeParams = new GestureStrokeParams(mainKeyboardViewAttr); - sGesturePreviewParams = new GestureStrokePreviewParams(mainKeyboardViewAttr); - sTimeRecorder = new TimeRecorder(sParams, sGestureStrokeParams); - } + sGestureStrokeRecognitionParams = new GestureStrokeRecognitionParams(mainKeyboardViewAttr); + sGestureStrokeDrawingParams = new GestureStrokeDrawingParams(mainKeyboardViewAttr); + sTypingTimeRecorder = new TypingTimeRecorder( + sGestureStrokeRecognitionParams.mStaticTimeThresholdAfterFastTyping, + sParams.mSuppressKeyPreviewAfterBatchInputDuration); - private static void updateGestureHandlingMode() { - sShouldHandleGesture = sMainDictionaryAvailable - && sGestureHandlingEnabledByInputField - && sGestureHandlingEnabledByUser - && !AccessibilityUtils.getInstance().isTouchExplorationEnabled(); + final Resources res = mainKeyboardViewAttr.getResources(); + sNeedsPhantomSuddenMoveEventHack = Boolean.parseBoolean( + ResourceUtils.getDeviceOverrideValue(res, + R.array.phantom_sudden_move_event_device_list, Boolean.FALSE.toString())); + BogusMoveEventDetector.init(res); + + sTimerProxy = timerProxy; + sDrawingProxy = drawingProxy; } // Note that this method is called from a non-UI thread. public static void setMainDictionaryAvailability(final boolean mainDictionaryAvailable) { - sMainDictionaryAvailable = mainDictionaryAvailable; - updateGestureHandlingMode(); + sGestureEnabler.setMainDictionaryAvailability(mainDictionaryAvailable); } public static void setGestureHandlingEnabledByUser(final boolean gestureHandlingEnabledByUser) { - sGestureHandlingEnabledByUser = gestureHandlingEnabledByUser; - updateGestureHandlingMode(); + sGestureEnabler.setGestureHandlingEnabledByUser(gestureHandlingEnabledByUser); } - public static PointerTracker getPointerTracker(final int id, final KeyEventHandler handler) { + public static PointerTracker getPointerTracker(final int id) { final ArrayList<PointerTracker> trackers = sTrackers; // Create pointer trackers until we can get 'id+1'-th tracker, if needed. for (int i = trackers.size(); i <= id; i++) { - final PointerTracker tracker = new PointerTracker(i, handler); + final PointerTracker tracker = new PointerTracker(i); trackers.add(tracker); } return trackers.get(id); } - public static boolean isAnyInSlidingKeyInput() { - return sPointerTrackerQueue.isAnyInSlidingKeyInput(); + public static boolean isAnyInDraggingFinger() { + return sPointerTrackerQueue.isAnyInDraggingFinger(); } public static void cancelAllPointerTrackers() { @@ -434,31 +250,27 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } public static void setKeyboardActionListener(final KeyboardActionListener listener) { - final int trackersSize = sTrackers.size(); - for (int i = 0; i < trackersSize; ++i) { - final PointerTracker tracker = sTrackers.get(i); - tracker.mListener = listener; - } + sListener = listener; } public static void setKeyDetector(final KeyDetector keyDetector) { + final Keyboard keyboard = keyDetector.getKeyboard(); + if (keyboard == null) { + return; + } final int trackersSize = sTrackers.size(); for (int i = 0; i < trackersSize; ++i) { final PointerTracker tracker = sTrackers.get(i); tracker.setKeyDetectorInner(keyDetector); - // Mark that keyboard layout has been changed. - tracker.mKeyboardLayoutHasBeenChanged = true; } - final Keyboard keyboard = keyDetector.getKeyboard(); - sGestureHandlingEnabledByInputField = !keyboard.mId.passwordInput(); - updateGestureHandlingMode(); + sGestureEnabler.setPasswordMode(keyboard.mId.passwordInput()); } public static void setReleasedKeyGraphicsToAllKeys() { final int trackersSize = sTrackers.size(); for (int i = 0; i < trackersSize; ++i) { final PointerTracker tracker = sTrackers.get(i); - tracker.setReleasedKeyGraphics(tracker.mCurrentKey); + tracker.setReleasedKeyGraphics(tracker.getKey()); } } @@ -466,28 +278,14 @@ public final class PointerTracker implements PointerTrackerQueue.Element { final int trackersSize = sTrackers.size(); for (int i = 0; i < trackersSize; ++i) { final PointerTracker tracker = sTrackers.get(i); - if (tracker.isShowingMoreKeysPanel()) { - tracker.mMoreKeysPanel.dismissMoreKeysPanel(); - tracker.mMoreKeysPanel = null; - } + tracker.dismissMoreKeysPanel(); } } - private PointerTracker(final int id, final KeyEventHandler handler) { - if (handler == null) { - throw new NullPointerException(); - } + private PointerTracker(final int id) { mPointerId = id; - mGestureStrokeWithPreviewPoints = new GestureStrokeWithPreviewPoints( - id, sGestureStrokeParams, sGesturePreviewParams); - setKeyEventHandler(handler); - } - - private void setKeyEventHandler(final KeyEventHandler handler) { - setKeyDetectorInner(handler.getKeyDetector()); - mListener = handler.getKeyboardActionListener(); - mDrawingProxy = handler.getDrawingProxy(); - mTimerProxy = handler.getTimerProxy(); + mBatchInputArbiter = new BatchInputArbiter(id, sGestureStrokeRecognitionParams); + mGestureStrokeDrawingPoints = new GestureStrokeDrawingPoints(sGestureStrokeDrawingParams); } // Returns true if keyboard has been changed by this callback. @@ -500,10 +298,10 @@ public final class PointerTracker implements PointerTrackerQueue.Element { if (sInGesture || mIsDetectingGesture || mIsTrackingForActionDisabled) { return false; } - final boolean ignoreModifierKey = mIsInSlidingKeyInput && key.isModifier(); + final boolean ignoreModifierKey = mIsInDraggingFinger && key.isModifier(); if (DEBUG_LISTENER) { Log.d(TAG, String.format("[%d] onPress : %s%s%s%s", mPointerId, - KeyDetector.printableCode(key), + (key == null ? "none" : Constants.printableCode(key.getCode())), ignoreModifierKey ? " ignoreModifier" : "", key.isEnabled() ? "" : " disabled", repeatCount > 0 ? " repeatCount=" + repeatCount : "")); @@ -512,21 +310,21 @@ public final class PointerTracker implements PointerTrackerQueue.Element { return false; } if (key.isEnabled()) { - mListener.onPressKey(key.getCode(), repeatCount, getActivePointerTrackerCount() == 1); + sListener.onPressKey(key.getCode(), repeatCount, getActivePointerTrackerCount() == 1); final boolean keyboardLayoutHasBeenChanged = mKeyboardLayoutHasBeenChanged; mKeyboardLayoutHasBeenChanged = false; - mTimerProxy.startTypingStateTimer(key); + sTimerProxy.startTypingStateTimer(key); return keyboardLayoutHasBeenChanged; } return false; } // Note that we need primaryCode argument because the keyboard may in shifted state and the - // primaryCode is different from {@link Key#mCode}. + // primaryCode is different from {@link Key#mKeyCode}. private void callListenerOnCodeInput(final Key key, final int primaryCode, final int x, - final int y, final long eventTime) { - final boolean ignoreModifierKey = mIsInSlidingKeyInput && key.isModifier(); - final boolean altersCode = key.altCodeWhileTyping() && mTimerProxy.isTypingState(); + final int y, final long eventTime, final boolean isKeyRepeat) { + final boolean ignoreModifierKey = mIsInDraggingFinger && key.isModifier(); + final boolean altersCode = key.altCodeWhileTyping() && sTimerProxy.isTypingState(); final int code = altersCode ? key.getAltCode() : primaryCode; if (DEBUG_LISTENER) { final String output = code == Constants.CODE_OUTPUT_TEXT @@ -535,48 +333,45 @@ public final class PointerTracker implements PointerTrackerQueue.Element { output, ignoreModifierKey ? " ignoreModifier" : "", altersCode ? " altersCode" : "", key.isEnabled() ? "" : " disabled")); } - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.pointerTracker_callListenerOnCodeInput(key, x, y, ignoreModifierKey, - altersCode, code); - } if (ignoreModifierKey) { return; } // Even if the key is disabled, it should respond if it is in the altCodeWhileTyping state. if (key.isEnabled() || altersCode) { - sTimeRecorder.onCodeInput(code, eventTime); + sTypingTimeRecorder.onCodeInput(code, eventTime); if (code == Constants.CODE_OUTPUT_TEXT) { - mListener.onTextInput(key.getOutputText()); + sListener.onTextInput(key.getOutputText()); } else if (code != Constants.CODE_UNSPECIFIED) { - mListener.onCodeInput(code, x, y); + if (mKeyboard.hasProximityCharsCorrection(code)) { + sListener.onCodeInput(code, x, y, isKeyRepeat); + } else { + sListener.onCodeInput(code, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, isKeyRepeat); + } } } } // Note that we need primaryCode argument because the keyboard may be in shifted state and the - // primaryCode is different from {@link Key#mCode}. + // primaryCode is different from {@link Key#mKeyCode}. private void callListenerOnRelease(final Key key, final int primaryCode, final boolean withSliding) { // See the comment at {@link #callListenerOnPressAndCheckKeyboardLayoutChange(Key}}. if (sInGesture || mIsDetectingGesture || mIsTrackingForActionDisabled) { return; } - final boolean ignoreModifierKey = mIsInSlidingKeyInput && key.isModifier(); + final boolean ignoreModifierKey = mIsInDraggingFinger && key.isModifier(); if (DEBUG_LISTENER) { Log.d(TAG, String.format("[%d] onRelease : %s%s%s%s", mPointerId, Constants.printableCode(primaryCode), withSliding ? " sliding" : "", ignoreModifierKey ? " ignoreModifier" : "", key.isEnabled() ? "": " disabled")); } - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.pointerTracker_callListenerOnRelease(key, primaryCode, withSliding, - ignoreModifierKey); - } if (ignoreModifierKey) { return; } if (key.isEnabled()) { - mListener.onReleaseKey(primaryCode, withSliding); + sListener.onReleaseKey(primaryCode, withSliding); } } @@ -584,43 +379,41 @@ public final class PointerTracker implements PointerTrackerQueue.Element { if (DEBUG_LISTENER) { Log.d(TAG, String.format("[%d] onFinishSlidingInput", mPointerId)); } - mListener.onFinishSlidingInput(); + sListener.onFinishSlidingInput(); } private void callListenerOnCancelInput() { if (DEBUG_LISTENER) { Log.d(TAG, String.format("[%d] onCancelInput", mPointerId)); } - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.pointerTracker_callListenerOnCancelInput(); - } - mListener.onCancelInput(); + sListener.onCancelInput(); } private void setKeyDetectorInner(final KeyDetector keyDetector) { final Keyboard keyboard = keyDetector.getKeyboard(); + if (keyboard == null) { + return; + } if (keyDetector == mKeyDetector && keyboard == mKeyboard) { return; } mKeyDetector = keyDetector; - mKeyboard = keyDetector.getKeyboard(); + mKeyboard = keyboard; + // Mark that keyboard layout has been changed. + mKeyboardLayoutHasBeenChanged = true; final int keyWidth = mKeyboard.mMostCommonKeyWidth; final int keyHeight = mKeyboard.mMostCommonKeyHeight; - mGestureStrokeWithPreviewPoints.setKeyboardGeometry(keyWidth, mKeyboard.mOccupiedHeight); - final Key newKey = mKeyDetector.detectHitKey(mKeyX, mKeyY); - if (newKey != mCurrentKey) { - if (mDrawingProxy != null) { - setReleasedKeyGraphics(mCurrentKey); - } - // Keep {@link #mCurrentKey} that comes from previous keyboard. - } - mPhantonSuddenMoveThreshold = (int)(keyWidth * PHANTOM_SUDDEN_MOVE_THRESHOLD); + mBatchInputArbiter.setKeyboardGeometry(keyWidth, mKeyboard.mOccupiedHeight); + // Keep {@link #mCurrentKey} that comes from previous keyboard. The key preview of + // {@link #mCurrentKey} will be dismissed by {@setReleasedKeyGraphics(Key)} via + // {@link onMoveEventInternal(int,int,long)} or {@link #onUpEventInternal(int,int,long)}. + mPhantomSuddenMoveThreshold = (int)(keyWidth * PHANTOM_SUDDEN_MOVE_THRESHOLD); mBogusMoveEventDetector.setKeyboardGeometry(keyWidth, keyHeight); } @Override - public boolean isInSlidingKeyInput() { - return mIsInSlidingKeyInput; + public boolean isInDraggingFinger() { + return mIsInDraggingFinger; } public Key getKey() { @@ -637,7 +430,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } private void setReleasedKeyGraphics(final Key key) { - mDrawingProxy.dismissKeyPreview(this); + sDrawingProxy.dismissKeyPreview(key); if (key == null) { return; } @@ -668,8 +461,8 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } private static boolean needsToSuppressKeyPreviewPopup(final long eventTime) { - if (!sShouldHandleGesture) return false; - return sTimeRecorder.needsToSuppressKeyPreviewPopup(eventTime); + if (!sGestureEnabler.shouldHandleGesture()) return false; + return sTypingTimeRecorder.needsToSuppressKeyPreviewPopup(eventTime); } private void setPressedKeyGraphics(final Key key, final long eventTime) { @@ -678,14 +471,14 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } // Even if the key is disabled, it should respond if it is in the altCodeWhileTyping state. - final boolean altersCode = key.altCodeWhileTyping() && mTimerProxy.isTypingState(); + final boolean altersCode = key.altCodeWhileTyping() && sTimerProxy.isTypingState(); final boolean needsToUpdateGraphics = key.isEnabled() || altersCode; if (!needsToUpdateGraphics) { return; } if (!key.noKeyPreview() && !sInGesture && !needsToSuppressKeyPreviewPopup(eventTime)) { - mDrawingProxy.showKeyPreview(this); + sDrawingProxy.showKeyPreview(key); } updatePressKeyGraphics(key); @@ -697,7 +490,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } } - if (key.altCodeWhileTyping() && mTimerProxy.isTypingState()) { + if (altersCode) { final int altCode = key.getAltCode(); final Key altKey = mKeyboard.getKey(altCode); if (altKey != null) { @@ -711,18 +504,18 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } } - private void updateReleaseKeyGraphics(final Key key) { + private static void updateReleaseKeyGraphics(final Key key) { key.onReleased(); - mDrawingProxy.invalidateKey(key); + sDrawingProxy.invalidateKey(key); } - private void updatePressKeyGraphics(final Key key) { + private static void updatePressKeyGraphics(final Key key) { key.onPressed(); - mDrawingProxy.invalidateKey(key); + sDrawingProxy.invalidateKey(key); } - public GestureStrokeWithPreviewPoints getGestureStrokeWithPreviewPoints() { - return mGestureStrokeWithPreviewPoints; + public GestureStrokeDrawingPoints getGestureStrokeDrawingPoints() { + return mGestureStrokeDrawingPoints; } public void getLastCoordinates(final int[] outCoords) { @@ -744,7 +537,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { return onMoveToNewKey(onMoveKeyInternal(x, y), x, y); } - static int getDistance(final int x1, final int y1, final int x2, final int y2) { + private static int getDistance(final int x1, final int y1, final int x2, final int y2) { return (int)Math.hypot(x1 - x2, y1 - y2); } @@ -766,7 +559,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { return newKey; } - private static int getActivePointerTrackerCount() { + /* package */ static int getActivePointerTrackerCount() { return sPointerTrackerQueue.size(); } @@ -774,91 +567,59 @@ public final class PointerTracker implements PointerTrackerQueue.Element { return sPointerTrackerQueue.getOldestElement() == this; } - private void mayStartBatchInput(final Key key) { - if (sInGesture || !mGestureStrokeWithPreviewPoints.isStartOfAGesture()) { - return; - } - if (key == null || !Character.isLetter(key.getCode())) { - return; - } + // Implements {@link BatchInputArbiterListener}. + @Override + public void onStartBatchInput() { if (DEBUG_LISTENER) { Log.d(TAG, String.format("[%d] onStartBatchInput", mPointerId)); } - sInGesture = true; - synchronized (sAggregratedPointers) { - sAggregratedPointers.reset(); - sLastRecognitionPointSize = 0; - sLastRecognitionTime = 0; - mListener.onStartBatchInput(); - dismissAllMoreKeysPanels(); - } - mTimerProxy.cancelLongPressTimer(); - // A gesture floating preview text will be shown at the oldest pointer/finger on the screen. - mDrawingProxy.showGestureTrail( - this, isOldestTrackerInQueue() /* showsFloatingPreviewText */); + sListener.onStartBatchInput(); + dismissAllMoreKeysPanels(); + sTimerProxy.cancelLongPressTimerOf(this); } - public void updateBatchInputByTimer(final long eventTime) { - final int gestureTime = (int)(eventTime - sGestureFirstDownTime); - mGestureStrokeWithPreviewPoints.duplicateLastPointWith(gestureTime); - updateBatchInput(eventTime); - } - - private void mayUpdateBatchInput(final long eventTime, final Key key) { - if (key != null) { - updateBatchInput(eventTime); - } + private void showGestureTrail() { if (mIsTrackingForActionDisabled) { return; } // A gesture floating preview text will be shown at the oldest pointer/finger on the screen. - mDrawingProxy.showGestureTrail( + sDrawingProxy.showGestureTrail( this, isOldestTrackerInQueue() /* showsFloatingPreviewText */); } - private void updateBatchInput(final long eventTime) { - synchronized (sAggregratedPointers) { - final GestureStroke stroke = mGestureStrokeWithPreviewPoints; - stroke.appendIncrementalBatchPoints(sAggregratedPointers); - final int size = sAggregratedPointers.getPointerSize(); - if (size > sLastRecognitionPointSize - && stroke.hasRecognitionTimePast(eventTime, sLastRecognitionTime)) { - if (DEBUG_LISTENER) { - Log.d(TAG, String.format("[%d] onUpdateBatchInput: batchPoints=%d", mPointerId, - size)); - } - mTimerProxy.startUpdateBatchInputTimer(this); - mListener.onUpdateBatchInput(sAggregratedPointers); - // The listener may change the size of the pointers (when auto-committing - // for example), so we need to get the size from the pointers again. - sLastRecognitionPointSize = sAggregratedPointers.getPointerSize(); - sLastRecognitionTime = eventTime; - } - } + public void updateBatchInputByTimer(final long syntheticMoveEventTime) { + mBatchInputArbiter.updateBatchInputByTimer(syntheticMoveEventTime, this); } - private void mayEndBatchInput(final long eventTime) { - synchronized (sAggregratedPointers) { - mGestureStrokeWithPreviewPoints.appendAllBatchPoints(sAggregratedPointers); - if (getActivePointerTrackerCount() == 1) { - sInGesture = false; - sTimeRecorder.onEndBatchInput(eventTime); - mTimerProxy.cancelAllUpdateBatchInputTimers(); - if (!mIsTrackingForActionDisabled) { - if (DEBUG_LISTENER) { - Log.d(TAG, String.format("[%d] onEndBatchInput : batchPoints=%d", - mPointerId, sAggregratedPointers.getPointerSize())); - } - mListener.onEndBatchInput(sAggregratedPointers); - } - } + // Implements {@link BatchInputArbiterListener}. + @Override + public void onUpdateBatchInput(final InputPointers aggregatedPointers, final long eventTime) { + if (DEBUG_LISTENER) { + Log.d(TAG, String.format("[%d] onUpdateBatchInput: batchPoints=%d", mPointerId, + aggregatedPointers.getPointerSize())); } + sListener.onUpdateBatchInput(aggregatedPointers); + } + + // Implements {@link BatchInputArbiterListener}. + @Override + public void onStartUpdateBatchInputTimer() { + sTimerProxy.startUpdateBatchInputTimer(this); + } + + // Implements {@link BatchInputArbiterListener}. + @Override + public void onEndBatchInput(final InputPointers aggregatedPointers, final long eventTime) { + sTypingTimeRecorder.onEndBatchInput(eventTime); + sTimerProxy.cancelAllUpdateBatchInputTimers(); if (mIsTrackingForActionDisabled) { return; } - // A gesture floating preview text will be shown at the oldest pointer/finger on the screen. - mDrawingProxy.showGestureTrail( - this, isOldestTrackerInQueue() /* showsFloatingPreviewText */); + if (DEBUG_LISTENER) { + Log.d(TAG, String.format("[%d] onEndBatchInput : batchPoints=%d", + mPointerId, aggregatedPointers.getPointerSize())); + } + sListener.onEndBatchInput(aggregatedPointers); } private void cancelBatchInput() { @@ -871,19 +632,26 @@ public final class PointerTracker implements PointerTrackerQueue.Element { if (DEBUG_LISTENER) { Log.d(TAG, String.format("[%d] onCancelBatchInput", mPointerId)); } - mListener.onCancelBatchInput(); + sListener.onCancelBatchInput(); } - public void processMotionEvent(final MotionEvent me, final KeyEventHandler handler) { + public void processMotionEvent(final MotionEvent me, final KeyDetector keyDetector) { final int action = me.getActionMasked(); final long eventTime = me.getEventTime(); if (action == MotionEvent.ACTION_MOVE) { + // When this pointer is the only active pointer and is showing a more keys panel, + // we should ignore other pointers' motion event. + final boolean shouldIgnoreOtherPointers = + isShowingMoreKeysPanel() && getActivePointerTrackerCount() == 1; final int pointerCount = me.getPointerCount(); for (int index = 0; index < pointerCount; index++) { final int id = me.getPointerId(index); - final PointerTracker tracker = getPointerTracker(id, handler); + if (shouldIgnoreOtherPointers && id != mPointerId) { + continue; + } final int x = (int)me.getX(index); final int y = (int)me.getY(index); + final PointerTracker tracker = getPointerTracker(id); tracker.onMoveEvent(x, y, eventTime, me); } return; @@ -894,7 +662,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { switch (action) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: - onDownEvent(x, y, eventTime, handler); + onDownEvent(x, y, eventTime, keyDetector); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: @@ -907,11 +675,11 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } private void onDownEvent(final int x, final int y, final long eventTime, - final KeyEventHandler handler) { + final KeyDetector keyDetector) { + setKeyDetectorInner(keyDetector); if (DEBUG_EVENT) { printTouchEvent("onDownEvent:", x, y, eventTime); } - setKeyEventHandler(handler); // Naive up-to-down noise filter. final long deltaT = eventTime - mUpTime; if (deltaT < sParams.mTouchNoiseThresholdTime) { @@ -921,9 +689,6 @@ public final class PointerTracker implements PointerTrackerQueue.Element { Log.w(TAG, String.format("[%d] onDownEvent:" + " ignore potential noise: time=%d distance=%d", mPointerId, deltaT, distance)); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.pointerTracker_onDownEvent(deltaT, distance * distance); - } cancelTrackingForAction(); return; } @@ -938,7 +703,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } sPointerTrackerQueue.add(this); onDownEventInternal(x, y, eventTime); - if (!sShouldHandleGesture) { + if (!sGestureEnabler.shouldHandleGesture()) { return; } // A gesture should start only from a non-modifier key. Note that the gesture detection is @@ -946,28 +711,36 @@ public final class PointerTracker implements PointerTrackerQueue.Element { mIsDetectingGesture = (mKeyboard != null) && mKeyboard.mId.isAlphabetKeyboard() && key != null && !key.isModifier(); if (mIsDetectingGesture) { - if (getActivePointerTrackerCount() == 1) { - sGestureFirstDownTime = eventTime; - } - mGestureStrokeWithPreviewPoints.onDownEvent(x, y, eventTime, sGestureFirstDownTime, - sTimeRecorder.getLastLetterTypingTime()); + mBatchInputArbiter.addDownEventPoint(x, y, eventTime, + sTypingTimeRecorder.getLastLetterTypingTime(), getActivePointerTrackerCount()); + mGestureStrokeDrawingPoints.onDownEvent( + x, y, mBatchInputArbiter.getElapsedTimeSinceFirstDown(eventTime)); } } - private boolean isShowingMoreKeysPanel() { + /* package */ boolean isShowingMoreKeysPanel() { return (mMoreKeysPanel != null); } + private void dismissMoreKeysPanel() { + if (isShowingMoreKeysPanel()) { + mMoreKeysPanel.dismissMoreKeysPanel(); + mMoreKeysPanel = null; + } + } + private void onDownEventInternal(final int x, final int y, final long eventTime) { Key key = onDownKey(x, y, eventTime); - // Sliding key is allowed when 1) enabled by configuration, 2) this pointer starts sliding - // from modifier key, or 3) this pointer's KeyDetector always allows sliding input. - mIsAllowedSlidingKeyInput = sParams.mSlidingKeyInputEnabled + // Key selection by dragging finger is allowed when 1) key selection by dragging finger is + // enabled by configuration, 2) this pointer starts dragging from modifier key, or 3) this + // pointer's KeyDetector always allows key selection by dragging finger, such as + // {@link MoreKeysKeyboard}. + mIsAllowedDraggingFinger = sParams.mKeySelectionByDraggingFinger || (key != null && key.isModifier()) - || mKeyDetector.alwaysAllowsSlidingInput(); + || mKeyDetector.alwaysAllowsKeySelectionByDraggingFinger(); mKeyboardLayoutHasBeenChanged = false; mIsTrackingForActionDisabled = false; - resetSlidingKeyInput(); + resetKeySelectionByDraggingFinger(); if (key != null) { // This onPress call may have changed keyboard layout. Those cases are detected at // {@link #setKeyboard}. In those cases, we should update key according to the new @@ -982,43 +755,47 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } } - private void startSlidingKeyInput(final Key key) { - if (!mIsInSlidingKeyInput) { - mIsInSlidingKeyInputFromModifier = key.isModifier(); + private void startKeySelectionByDraggingFinger(final Key key) { + if (!mIsInDraggingFinger) { + mIsInSlidingKeyInput = key.isModifier(); } - mIsInSlidingKeyInput = true; + mIsInDraggingFinger = true; } - private void resetSlidingKeyInput() { + private void resetKeySelectionByDraggingFinger() { + mIsInDraggingFinger = false; mIsInSlidingKeyInput = false; - mIsInSlidingKeyInputFromModifier = false; - mDrawingProxy.dismissSlidingKeyInputPreview(); + sDrawingProxy.dismissSlidingKeyInputPreview(); } private void onGestureMoveEvent(final int x, final int y, final long eventTime, final boolean isMajorEvent, final Key key) { - final int gestureTime = (int)(eventTime - sGestureFirstDownTime); - if (mIsDetectingGesture) { - final int beforeLength = mGestureStrokeWithPreviewPoints.getLength(); - final boolean onValidArea = mGestureStrokeWithPreviewPoints.addPointOnKeyboard( - x, y, gestureTime, isMajorEvent); - if (mGestureStrokeWithPreviewPoints.getLength() > beforeLength) { - mTimerProxy.startUpdateBatchInputTimer(this); - } - // If the move event goes out from valid batch input area, cancel batch input. - if (!onValidArea) { - cancelBatchInput(); - return; - } - // If the MoreKeysPanel is showing then do not attempt to enter gesture mode. However, - // the gestured touch points are still being recorded in case the panel is dismissed. - if (isShowingMoreKeysPanel()) { - return; - } - mayStartBatchInput(key); - if (sInGesture) { - mayUpdateBatchInput(eventTime, key); + if (!mIsDetectingGesture) { + return; + } + final boolean onValidArea = mBatchInputArbiter.addMoveEventPoint( + x, y, eventTime, isMajorEvent, this); + // If the move event goes out from valid batch input area, cancel batch input. + if (!onValidArea) { + cancelBatchInput(); + return; + } + mGestureStrokeDrawingPoints.onMoveEvent( + x, y, mBatchInputArbiter.getElapsedTimeSinceFirstDown(eventTime)); + // If the MoreKeysPanel is showing then do not attempt to enter gesture mode. However, + // the gestured touch points are still being recorded in case the panel is dismissed. + if (isShowingMoreKeysPanel()) { + return; + } + if (!sInGesture && key != null && Character.isLetter(key.getCode()) + && mBatchInputArbiter.mayStartBatchInput(this)) { + sInGesture = true; + } + if (sInGesture) { + if (key != null) { + mBatchInputArbiter.updateBatchInput(eventTime, this); } + showGestureTrail(); } } @@ -1030,7 +807,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { return; } - if (sShouldHandleGesture && me != null) { + if (sGestureEnabler.shouldHandleGesture() && me != null) { // Add historical points to gesture path. final int pointerIndex = me.findPointerIndex(mPointerId); final int historicalSize = me.getHistorySize(); @@ -1048,15 +825,15 @@ public final class PointerTracker implements PointerTrackerQueue.Element { final int translatedY = mMoreKeysPanel.translateY(y); mMoreKeysPanel.onMoveEvent(translatedX, translatedY, mPointerId, eventTime); onMoveKey(x, y); - if (mIsInSlidingKeyInputFromModifier) { - mDrawingProxy.showSlidingKeyInputPreview(this); + if (mIsInSlidingKeyInput) { + sDrawingProxy.showSlidingKeyInputPreview(this); } return; } onMoveEventInternal(x, y, eventTime); } - private void processSlidingKeyInput(final Key newKey, final int x, final int y, + private void processDraggingFingerInToNewKey(final Key newKey, final int x, final int y, final long eventTime) { // This onPress call may have changed keyboard layout. Those cases are detected // at {@link #setKeyboard}. In those cases, we should update key according @@ -1083,10 +860,6 @@ public final class PointerTracker implements PointerTrackerQueue.Element { lastX, lastY, Constants.printableCode(oldKey.getCode()), x, y, Constants.printableCode(key.getCode()))); } - // TODO: This should be moved to outside of this nested if-clause? - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.pointerTracker_onMoveEvent(x, y, lastX, lastY); - } onUpEventInternal(x, y, eventTime); onDownEventInternal(x, y, eventTime); } @@ -1110,35 +883,35 @@ public final class PointerTracker implements PointerTrackerQueue.Element { onDownEventInternal(x, y, eventTime); } - private void processSildeOutFromOldKey(final Key oldKey) { + private void processDraggingFingerOutFromOldKey(final Key oldKey) { setReleasedKeyGraphics(oldKey); callListenerOnRelease(oldKey, oldKey.getCode(), true /* withSliding */); - startSlidingKeyInput(oldKey); - mTimerProxy.cancelKeyTimers(); + startKeySelectionByDraggingFinger(oldKey); + sTimerProxy.cancelKeyTimersOf(this); } - private void slideFromOldKeyToNewKey(final Key key, final int x, final int y, + private void dragFingerFromOldKeyToNewKey(final Key key, final int x, final int y, final long eventTime, final Key oldKey, final int lastX, final int lastY) { // The pointer has been slid in to the new key from the previous key, we must call // onRelease() first to notify that the previous key has been released, then call // onPress() to notify that the new key is being pressed. - processSildeOutFromOldKey(oldKey); + processDraggingFingerOutFromOldKey(oldKey); startRepeatKey(key); - if (mIsAllowedSlidingKeyInput) { - processSlidingKeyInput(key, x, y, eventTime); + if (mIsAllowedDraggingFinger) { + processDraggingFingerInToNewKey(key, x, y, eventTime); } // HACK: On some devices, quick successive touches may be reported as a sudden move by // touch panel firmware. This hack detects such cases and translates the move event to // successive up and down events. // TODO: Should find a way to balance gesture detection and this hack. else if (sNeedsPhantomSuddenMoveEventHack - && getDistance(x, y, lastX, lastY) >= mPhantonSuddenMoveThreshold) { + && getDistance(x, y, lastX, lastY) >= mPhantomSuddenMoveThreshold) { processPhantomSuddenMoveHack(key, x, y, eventTime, oldKey, lastX, lastY); } // HACK: On some devices, quick successive proximate touches may be reported as a bogus // down-move-up event by touch panel firmware. This hack detects such cases and breaks // these events into separate up and down events. - else if (sNeedsProximateBogusDownMoveUpEventHack && sTimeRecorder.isInFastTyping(eventTime) + else if (sTypingTimeRecorder.isInFastTyping(eventTime) && mBogusMoveEventDetector.isCloseToActualDownEvent(x, y)) { processProximateBogusDownMoveUpEventHack(key, x, y, eventTime, oldKey, lastX, lastY); } @@ -1163,11 +936,11 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } } - private void slideOutFromOldKey(final Key oldKey, final int x, final int y) { + private void dragFingerOutFromOldKey(final Key oldKey, final int x, final int y) { // The pointer has been slid out from the previous key, we must call onRelease() to // notify that the previous key has been released. - processSildeOutFromOldKey(oldKey); - if (mIsAllowedSlidingKeyInput) { + processDraggingFingerOutFromOldKey(oldKey); + if (mIsAllowedDraggingFinger) { onMoveToNewKey(null, x, y); } else { if (!mIsDetectingGesture) { @@ -1182,7 +955,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { final Key oldKey = mCurrentKey; final Key newKey = onMoveKey(x, y); - if (sShouldHandleGesture) { + if (sGestureEnabler.shouldHandleGesture()) { // Register move event on gesture tracker. onGestureMoveEvent(x, y, eventTime, true /* isMajorEvent */, newKey); if (sInGesture) { @@ -1194,19 +967,19 @@ public final class PointerTracker implements PointerTrackerQueue.Element { if (newKey != null) { if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, eventTime, newKey)) { - slideFromOldKeyToNewKey(newKey, x, y, eventTime, oldKey, lastX, lastY); + dragFingerFromOldKeyToNewKey(newKey, x, y, eventTime, oldKey, lastX, lastY); } else if (oldKey == null) { // The pointer has been slid in to the new key, but the finger was not on any keys. // In this case, we must call onPress() to notify that the new key is being pressed. - processSlidingKeyInput(newKey, x, y, eventTime); + processDraggingFingerInToNewKey(newKey, x, y, eventTime); } } else { // newKey == null if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, eventTime, newKey)) { - slideOutFromOldKey(oldKey, x, y); + dragFingerOutFromOldKey(oldKey, x, y); } } - if (mIsInSlidingKeyInputFromModifier) { - mDrawingProxy.showSlidingKeyInputPreview(this); + if (mIsInSlidingKeyInput) { + sDrawingProxy.showSlidingKeyInputPreview(this); } } @@ -1215,7 +988,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { printTouchEvent("onUpEvent :", x, y, eventTime); } - mTimerProxy.cancelUpdateBatchInputTimer(this); + sTimerProxy.cancelUpdateBatchInputTimer(this); if (!sInGesture) { if (mCurrentKey != null && mCurrentKey.isModifier()) { // Before processing an up event of modifier key, all pointers already being @@ -1237,18 +1010,15 @@ public final class PointerTracker implements PointerTrackerQueue.Element { if (DEBUG_EVENT) { printTouchEvent("onPhntEvent:", mLastX, mLastY, eventTime); } - if (isShowingMoreKeysPanel()) { - return; - } onUpEventInternal(mLastX, mLastY, eventTime); cancelTrackingForAction(); } private void onUpEventInternal(final int x, final int y, final long eventTime) { - mTimerProxy.cancelKeyTimers(); + sTimerProxy.cancelKeyTimersOf(this); + final boolean isInDraggingFinger = mIsInDraggingFinger; final boolean isInSlidingKeyInput = mIsInSlidingKeyInput; - final boolean isInSlidingKeyInputFromModifier = mIsInSlidingKeyInputFromModifier; - resetSlidingKeyInput(); + resetKeySelectionByDraggingFinger(); mIsDetectingGesture = false; final Key currentKey = mCurrentKey; mCurrentKey = null; @@ -1263,8 +1033,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { final int translatedY = mMoreKeysPanel.translateY(y); mMoreKeysPanel.onUpEvent(translatedX, translatedY, mPointerId, eventTime); } - mMoreKeysPanel.dismissMoreKeysPanel(); - mMoreKeysPanel = null; + dismissMoreKeysPanel(); return; } @@ -1272,7 +1041,11 @@ public final class PointerTracker implements PointerTrackerQueue.Element { if (currentKey != null) { callListenerOnRelease(currentKey, currentKey.getCode(), true /* withSliding */); } - mayEndBatchInput(eventTime); + if (mBatchInputArbiter.mayEndBatchInput( + eventTime, getActivePointerTrackerCount(), this)) { + sInGesture = false; + } + showGestureTrail(); return; } @@ -1280,11 +1053,11 @@ public final class PointerTracker implements PointerTrackerQueue.Element { return; } if (currentKey != null && currentKey.isRepeatable() - && (currentKey.getCode() == currentRepeatingKeyCode) && !isInSlidingKeyInput) { + && (currentKey.getCode() == currentRepeatingKeyCode) && !isInDraggingFinger) { return; } detectAndSendKey(currentKey, mKeyX, mKeyY, eventTime); - if (isInSlidingKeyInputFromModifier) { + if (isInSlidingKeyInput) { callListenerOnFinishSlidingInput(); } } @@ -1305,8 +1078,16 @@ public final class PointerTracker implements PointerTrackerQueue.Element { mIsTrackingForActionDisabled = true; } + public boolean isInOperation() { + return !mIsTrackingForActionDisabled; + } + + public void cancelLongPressTimer() { + sTimerProxy.cancelLongPressTimerOf(this); + } + public void onLongPressed() { - resetSlidingKeyInput(); + resetKeySelectionByDraggingFinger(); cancelTrackingForAction(); setReleasedKeyGraphics(mCurrentKey); sPointerTrackerQueue.remove(this); @@ -1324,20 +1105,14 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } private void onCancelEventInternal() { - mTimerProxy.cancelKeyTimers(); + sTimerProxy.cancelKeyTimersOf(this); setReleasedKeyGraphics(mCurrentKey); - resetSlidingKeyInput(); - if (isShowingMoreKeysPanel()) { - mMoreKeysPanel.dismissMoreKeysPanel(); - mMoreKeysPanel = null; - } + resetKeySelectionByDraggingFinger(); + dismissMoreKeysPanel(); } private boolean isMajorEnoughMoveToBeOnNewKey(final int x, final int y, final long eventTime, final Key newKey) { - if (mKeyDetector == null) { - throw new NullPointerException("keyboard and/or key detector not set"); - } final Key curKey = mCurrentKey; if (newKey == curKey) { return false; @@ -1347,7 +1122,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } // Here curKey points to the different key from newKey. final int keyHysteresisDistanceSquared = mKeyDetector.getKeyHysteresisDistanceSquared( - mIsInSlidingKeyInputFromModifier); + mIsInSlidingKeyInput); final int distanceFromKeyEdgeSquared = curKey.squaredDistanceToEdge(x, y); if (distanceFromKeyEdgeSquared >= keyHysteresisDistanceSquared) { if (DEBUG_MODE) { @@ -1358,14 +1133,13 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } return true; } - if (sNeedsProximateBogusDownMoveUpEventHack && !mIsAllowedSlidingKeyInput - && sTimeRecorder.isInFastTyping(eventTime) + if (!mIsAllowedDraggingFinger && sTypingTimeRecorder.isInFastTyping(eventTime) && mBogusMoveEventDetector.hasTraveledLongDistance(x, y)) { if (DEBUG_MODE) { final float keyDiagonal = (float)Math.hypot( mKeyboard.mMostCommonKeyWidth, mKeyboard.mMostCommonKeyHeight); final float lengthFromDownRatio = - mBogusMoveEventDetector.mAccumulatedDistanceFromDownKey / keyDiagonal; + mBogusMoveEventDetector.getAccumulatedDistanceFromDownKey() / keyDiagonal; Log.d(TAG, String.format("[%d] isMajorEnoughMoveToBeOnNewKey:" + " %.2f key diagonal from virtual down point", mPointerId, lengthFromDownRatio)); @@ -1376,30 +1150,34 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } private void startLongPressTimer(final Key key) { + // Note that we need to cancel all active long press shift key timers if any whenever we + // start a new long press timer for both non-shift and shift keys. + sTimerProxy.cancelLongPressShiftKeyTimers(); if (sInGesture) return; if (key == null) return; if (!key.isLongPressEnabled()) return; // Caveat: Please note that isLongPressEnabled() can be true even if the current key - // doesn't have its more keys. (e.g. spacebar, globe key) + // doesn't have its more keys. (e.g. spacebar, globe key) If we are in the dragging finger + // mode, we will disable long press timer of such key. // We always need to start the long press timer if the key has its more keys regardless of - // whether or not we are in the sliding input mode. - if (mIsInSlidingKeyInput && key.getMoreKeys() == null) return; - final int delay; - switch (key.getCode()) { - case Constants.CODE_SHIFT: - delay = sParams.mLongPressShiftLockTimeout; - break; - default: - final int longpressTimeout = Settings.getInstance().getCurrent().mKeyLongpressTimeout; - if (mIsInSlidingKeyInputFromModifier) { - // We use longer timeout for sliding finger input started from the modifier key. - delay = longpressTimeout * MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT; - } else { - delay = longpressTimeout; - } - break; + // whether or not we are in the dragging finger mode. + if (mIsInDraggingFinger && key.getMoreKeys() == null) return; + + final int delay = getLongPressTimeout(key.getCode()); + if (delay <= 0) return; + sTimerProxy.startLongPressTimerOf(this, delay); + } + + private int getLongPressTimeout(final int code) { + if (code == Constants.CODE_SHIFT) { + return sParams.mLongPressShiftLockTimeout; + } + final int longpressTimeout = Settings.getInstance().getCurrent().mKeyLongpressTimeout; + if (mIsInSlidingKeyInput) { + // We use longer timeout for sliding finger input started from the modifier key. + return longpressTimeout * MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT; } - mTimerProxy.startLongPressTimer(this, delay); + return longpressTimeout; } private void detectAndSendKey(final Key key, final int x, final int y, final long eventTime) { @@ -1409,7 +1187,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } final int code = key.getCode(); - callListenerOnCodeInput(key, code, x, y, eventTime); + callListenerOnCodeInput(key, code, x, y, eventTime, false /* isKeyRepeat */); callListenerOnRelease(key, code, false /* withSliding */); } @@ -1417,10 +1195,10 @@ public final class PointerTracker implements PointerTrackerQueue.Element { if (sInGesture) return; if (key == null) return; if (!key.isRepeatable()) return; - // Don't start key repeat when we are in sliding input mode. - if (mIsInSlidingKeyInput) return; + // Don't start key repeat when we are in the dragging finger mode. + if (mIsInDraggingFinger) return; final int startRepeatCount = 1; - mTimerProxy.startKeyRepeatTimer(this, startRepeatCount, sParams.mKeyRepeatStartTimeout); + startKeyRepeatTimer(startRepeatCount); } public void onKeyRepeat(final int code, final int repeatCount) { @@ -1432,15 +1210,22 @@ public final class PointerTracker implements PointerTrackerQueue.Element { mCurrentRepeatingKeyCode = code; mIsDetectingGesture = false; final int nextRepeatCount = repeatCount + 1; - mTimerProxy.startKeyRepeatTimer(this, nextRepeatCount, sParams.mKeyRepeatInterval); + startKeyRepeatTimer(nextRepeatCount); callListenerOnPressAndCheckKeyboardLayoutChange(key, repeatCount); - callListenerOnCodeInput(key, code, mKeyX, mKeyY, SystemClock.uptimeMillis()); + callListenerOnCodeInput(key, code, mKeyX, mKeyY, SystemClock.uptimeMillis(), + true /* isKeyRepeat */); + } + + private void startKeyRepeatTimer(final int repeatCount) { + final int delay = + (repeatCount == 1) ? sParams.mKeyRepeatStartTimeout : sParams.mKeyRepeatInterval; + sTimerProxy.startKeyRepeatTimerOf(this, repeatCount, delay); } private void printTouchEvent(final String title, final int x, final int y, final long eventTime) { final Key key = mKeyDetector.detectHitKey(x, y); - final String code = KeyDetector.printableCode(key); + final String code = (key == null ? "none" : Constants.printableCode(key.getCode())); Log.d(TAG, String.format("[%d]%s%s %4d %4d %5d %s", mPointerId, (mIsTrackingForActionDisabled ? "-" : " "), title, x, y, eventTime, code)); } diff --git a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java index a0316696c..c19cd671a 100644 --- a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java +++ b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java @@ -24,7 +24,10 @@ import com.android.inputmethod.keyboard.internal.TouchPositionCorrection; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.utils.JniUtils; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.List; public class ProximityInfo { private static final String TAG = ProximityInfo.class.getSimpleName(); @@ -34,7 +37,7 @@ public class ProximityInfo { public static final int MAX_PROXIMITY_CHARS_SIZE = 16; /** Number of key widths from current touch point to search for nearest keys. */ private static final float SEARCH_DISTANCE = 1.2f; - private static final Key[] EMPTY_KEY_ARRAY = new Key[0]; + private static final List<Key> EMPTY_KEY_LIST = Collections.emptyList(); private static final float DEFAULT_TOUCH_POSITION_CORRECTION_RADIUS = 0.15f; private final int mGridWidth; @@ -47,13 +50,14 @@ public class ProximityInfo { private final int mKeyboardHeight; private final int mMostCommonKeyWidth; private final int mMostCommonKeyHeight; - private final Key[] mKeys; - private final Key[][] mGridNeighbors; + private final List<Key> mSortedKeys; + private final List<Key>[] mGridNeighbors; private final String mLocaleStr; + @SuppressWarnings("unchecked") ProximityInfo(final String localeStr, final int gridWidth, final int gridHeight, final int minWidth, final int height, final int mostCommonKeyWidth, - final int mostCommonKeyHeight, final Key[] keys, + final int mostCommonKeyHeight, final List<Key> sortedKeys, final TouchPositionCorrection touchPositionCorrection) { if (TextUtils.isEmpty(localeStr)) { mLocaleStr = ""; @@ -69,8 +73,8 @@ public class ProximityInfo { mKeyboardHeight = height; mMostCommonKeyHeight = mostCommonKeyHeight; mMostCommonKeyWidth = mostCommonKeyWidth; - mKeys = keys; - mGridNeighbors = new Key[mGridSize][]; + mSortedKeys = sortedKeys; + mGridNeighbors = new List[mGridSize]; if (minWidth == 0 || height == 0) { // No proximity required. Keyboard might be more keys keyboard. return; @@ -99,7 +103,7 @@ public class ProximityInfo { return key.getCode() >= Constants.CODE_SPACE; } - private static int getProximityInfoKeysCount(final Key[] keys) { + private static int getProximityInfoKeysCount(final List<Key> keys) { int count = 0; for (final Key key : keys) { if (needsProximityInfo(key)) { @@ -110,14 +114,15 @@ public class ProximityInfo { } private long createNativeProximityInfo(final TouchPositionCorrection touchPositionCorrection) { - final Key[][] gridNeighborKeys = mGridNeighbors; + final List<Key>[] gridNeighborKeys = mGridNeighbors; final int[] proximityCharsArray = new int[mGridSize * MAX_PROXIMITY_CHARS_SIZE]; Arrays.fill(proximityCharsArray, Constants.NOT_A_CODE); for (int i = 0; i < mGridSize; ++i) { - final int proximityCharsLength = gridNeighborKeys[i].length; + final List<Key> neighborKeys = gridNeighborKeys[i]; + final int proximityCharsLength = neighborKeys.size(); int infoIndex = i * MAX_PROXIMITY_CHARS_SIZE; for (int j = 0; j < proximityCharsLength; ++j) { - final Key neighborKey = gridNeighborKeys[i][j]; + final Key neighborKey = neighborKeys.get(j); // Excluding from proximityCharsArray if (!needsProximityInfo(neighborKey)) { continue; @@ -142,8 +147,8 @@ public class ProximityInfo { } } - final Key[] keys = mKeys; - final int keyCount = getProximityInfoKeysCount(keys); + final List<Key> sortedKeys = mSortedKeys; + final int keyCount = getProximityInfoKeysCount(sortedKeys); final int[] keyXCoordinates = new int[keyCount]; final int[] keyYCoordinates = new int[keyCount]; final int[] keyWidths = new int[keyCount]; @@ -153,8 +158,8 @@ public class ProximityInfo { final float[] sweetSpotCenterYs; final float[] sweetSpotRadii; - for (int infoIndex = 0, keyIndex = 0; keyIndex < keys.length; keyIndex++) { - final Key key = keys[keyIndex]; + for (int infoIndex = 0, keyIndex = 0; keyIndex < sortedKeys.size(); keyIndex++) { + final Key key = sortedKeys.get(keyIndex); // Excluding from key coordinate arrays if (!needsProximityInfo(key)) { continue; @@ -177,8 +182,8 @@ public class ProximityInfo { final int rows = touchPositionCorrection.getRows(); final float defaultRadius = DEFAULT_TOUCH_POSITION_CORRECTION_RADIUS * (float)Math.hypot(mMostCommonKeyWidth, mMostCommonKeyHeight); - for (int infoIndex = 0, keyIndex = 0; keyIndex < keys.length; keyIndex++) { - final Key key = keys[keyIndex]; + for (int infoIndex = 0, keyIndex = 0; keyIndex < sortedKeys.size(); keyIndex++) { + final Key key = sortedKeys.get(keyIndex); // Excluding from touch position correction arrays if (!needsProximityInfo(key)) { continue; @@ -240,7 +245,7 @@ public class ProximityInfo { private void computeNearestNeighbors() { final int defaultWidth = mMostCommonKeyWidth; - final int keyCount = mKeys.length; + final int keyCount = mSortedKeys.size(); final int gridSize = mGridNeighbors.length; final int threshold = (int) (defaultWidth * SEARCH_DISTANCE); final int thresholdSquared = threshold * threshold; @@ -259,7 +264,7 @@ public class ProximityInfo { final int[] neighborCountPerCell = new int[gridSize]; final int halfCellWidth = mCellWidth / 2; final int halfCellHeight = mCellHeight / 2; - for (final Key key : mKeys) { + for (final Key key : mSortedKeys) { if (key.isSpacer()) continue; /* HOW WE PRE-SELECT THE CELLS (iterate over only the relevant cells, instead of all of them) @@ -353,9 +358,13 @@ y |---+---+---+---+-v-+-|-+---+---+---+---+---| | thresholdBase and get } for (int i = 0; i < gridSize; ++i) { - final int base = i * keyCount; - mGridNeighbors[i] = - Arrays.copyOfRange(neighborsFlatBuffer, base, base + neighborCountPerCell[i]); + final int indexStart = i * keyCount; + final int indexEnd = indexStart + neighborCountPerCell[i]; + final ArrayList<Key> neighbors = new ArrayList<>(indexEnd - indexStart); + for (int index = indexStart; index < indexEnd; index++) { + neighbors.add(neighborsFlatBuffer[index]); + } + mGridNeighbors[i] = Collections.unmodifiableList(neighbors); } } @@ -369,7 +378,7 @@ y |---+---+---+---+-v-+-|-+---+---+---+---+---| | thresholdBase and get if (primaryKeyCode > Constants.CODE_SPACE) { dest[index++] = primaryKeyCode; } - final Key[] nearestKeys = getNearestKeys(x, y); + final List<Key> nearestKeys = getNearestKeys(x, y); for (Key key : nearestKeys) { if (index >= destLength) { break; @@ -385,9 +394,9 @@ y |---+---+---+---+-v-+-|-+---+---+---+---+---| | thresholdBase and get } } - public Key[] getNearestKeys(final int x, final int y) { + public List<Key> getNearestKeys(final int x, final int y) { if (mGridNeighbors == null) { - return EMPTY_KEY_ARRAY; + return EMPTY_KEY_LIST; } if (x >= 0 && x < mKeyboardMinWidth && y >= 0 && y < mKeyboardHeight) { int index = (y / mCellHeight) * mGridWidth + (x / mCellWidth); @@ -395,6 +404,6 @@ y |---+---+---+---+-v-+-|-+---+---+---+---+---| | thresholdBase and get return mGridNeighbors[index]; } } - return EMPTY_KEY_ARRAY; + return EMPTY_KEY_LIST; } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java b/java/src/com/android/inputmethod/keyboard/emoji/DynamicGridKeyboard.java index 3133e54be..daeb1f928 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java +++ b/java/src/com/android/inputmethod/keyboard/emoji/DynamicGridKeyboard.java @@ -14,28 +14,27 @@ * limitations under the License. */ -package com.android.inputmethod.keyboard.internal; +package com.android.inputmethod.keyboard.emoji; import android.content.SharedPreferences; import android.text.TextUtils; import android.util.Log; -import com.android.inputmethod.keyboard.EmojiPalettesView; import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.latin.settings.Settings; -import com.android.inputmethod.latin.utils.CollectionUtils; -import com.android.inputmethod.latin.utils.StringUtils; +import com.android.inputmethod.latin.utils.JsonUtils; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; /** * This is a Keyboard class where you can add keys dynamically shown in a grid layout */ -public class DynamicGridKeyboard extends Keyboard { +final class DynamicGridKeyboard extends Keyboard { private static final String TAG = DynamicGridKeyboard.class.getSimpleName(); private static final int TEMPLATE_KEY_CODE_0 = 0x30; private static final int TEMPLATE_KEY_CODE_1 = 0x31; @@ -47,13 +46,13 @@ public class DynamicGridKeyboard extends Keyboard { private final int mColumnsNum; private final int mMaxKeyCount; private final boolean mIsRecents; - private final ArrayDeque<GridKey> mGridKeys = CollectionUtils.newArrayDeque(); - private final ArrayDeque<Key> mPendingKeys = CollectionUtils.newArrayDeque(); + private final ArrayDeque<GridKey> mGridKeys = new ArrayDeque<>(); + private final ArrayDeque<Key> mPendingKeys = new ArrayDeque<>(); - private Key[] mCachedGridKeys; + private List<Key> mCachedGridKeys; public DynamicGridKeyboard(final SharedPreferences prefs, final Keyboard templateKeyboard, - final int maxKeyCount, final int categoryId, final int categoryPageId) { + final int maxKeyCount, final int categoryId) { super(templateKeyboard); final Key key0 = getTemplateKey(TEMPLATE_KEY_CODE_0); final Key key1 = getTemplateKey(TEMPLATE_KEY_CODE_1); @@ -61,12 +60,12 @@ public class DynamicGridKeyboard extends Keyboard { mVerticalStep = key0.getHeight() + mVerticalGap; mColumnsNum = mBaseWidth / mHorizontalStep; mMaxKeyCount = maxKeyCount; - mIsRecents = categoryId == EmojiPalettesView.CATEGORY_ID_RECENTS; + mIsRecents = categoryId == EmojiCategory.ID_RECENTS; mPrefs = prefs; } private Key getTemplateKey(final int code) { - for (final Key key : super.getKeys()) { + for (final Key key : super.getSortedKeys()) { if (key.getCode() == code) { return key; } @@ -124,14 +123,14 @@ public class DynamicGridKeyboard extends Keyboard { final int keyY0 = getKeyY0(index); final int keyX1 = getKeyX1(index); final int keyY1 = getKeyY1(index); - gridKey.updateCorrdinates(keyX0, keyY0, keyX1, keyY1); + gridKey.updateCoordinates(keyX0, keyY0, keyX1, keyY1); index++; } } } private void saveRecentKeys() { - final ArrayList<Object> keys = CollectionUtils.newArrayList(); + final ArrayList<Object> keys = new ArrayList<>(); for (final Key key : mGridKeys) { if (key.getOutputText() != null) { keys.add(key.getOutputText()); @@ -139,36 +138,50 @@ public class DynamicGridKeyboard extends Keyboard { keys.add(key.getCode()); } } - final String jsonStr = StringUtils.listToJsonStr(keys); + final String jsonStr = JsonUtils.listToJsonStr(keys); Settings.writeEmojiRecentKeys(mPrefs, jsonStr); } - private static Key getKey(final Collection<DynamicGridKeyboard> keyboards, final Object o) { - for (final DynamicGridKeyboard kbd : keyboards) { - if (o instanceof Integer) { - final int code = (Integer) o; - final Key key = kbd.getKey(code); - if (key != null) { + private static Key getKeyByCode(final Collection<DynamicGridKeyboard> keyboards, + final int code) { + for (final DynamicGridKeyboard keyboard : keyboards) { + for (final Key key : keyboard.getSortedKeys()) { + if (key.getCode() == code) { return key; } - } else if (o instanceof String) { - final String outputText = (String) o; - final Key key = kbd.getKeyFromOutputText(outputText); - if (key != null) { + } + } + return null; + } + + private static Key getKeyByOutputText(final Collection<DynamicGridKeyboard> keyboards, + final String outputText) { + for (final DynamicGridKeyboard keyboard : keyboards) { + for (final Key key : keyboard.getSortedKeys()) { + if (outputText.equals(key.getOutputText())) { return key; } - } else { - Log.w(TAG, "Invalid object: " + o); } } return null; } - public void loadRecentKeys(Collection<DynamicGridKeyboard> keyboards) { + public void loadRecentKeys(final Collection<DynamicGridKeyboard> keyboards) { final String str = Settings.readEmojiRecentKeys(mPrefs); - final List<Object> keys = StringUtils.jsonStrToList(str); + final List<Object> keys = JsonUtils.jsonStrToList(str); for (final Object o : keys) { - addKeyLast(getKey(keyboards, o)); + final Key key; + if (o instanceof Integer) { + final int code = (Integer)o; + key = getKeyByCode(keyboards, code); + } else if (o instanceof String) { + final String outputText = (String)o; + key = getKeyByOutputText(keyboards, outputText); + } else { + Log.w(TAG, "Invalid object: " + o); + continue; + } + addKeyLast(key); } } @@ -193,20 +206,21 @@ public class DynamicGridKeyboard extends Keyboard { } @Override - public Key[] getKeys() { + public List<Key> getSortedKeys() { synchronized (mLock) { if (mCachedGridKeys != null) { return mCachedGridKeys; } - mCachedGridKeys = mGridKeys.toArray(new Key[mGridKeys.size()]); + final ArrayList<Key> cachedKeys = new ArrayList<Key>(mGridKeys); + mCachedGridKeys = Collections.unmodifiableList(cachedKeys); return mCachedGridKeys; } } @Override - public Key[] getNearestKeys(final int x, final int y) { + public List<Key> getNearestKeys(final int x, final int y) { // TODO: Calculate the nearest key index in mGridKeys from x and y. - return getKeys(); + return getSortedKeys(); } static final class GridKey extends Key { @@ -217,7 +231,7 @@ public class DynamicGridKeyboard extends Keyboard { super(originalKey); } - public void updateCorrdinates(final int x0, final int y0, final int x1, final int y1) { + public void updateCoordinates(final int x0, final int y0, final int x1, final int y1) { mCurrentX = x0; mCurrentY = y0; getHitBox().set(x0, y0, x1, y1); diff --git a/java/src/com/android/inputmethod/keyboard/emoji/EmojiCategory.java b/java/src/com/android/inputmethod/keyboard/emoji/EmojiCategory.java new file mode 100644 index 000000000..512d4615d --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/emoji/EmojiCategory.java @@ -0,0 +1,353 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.emoji; + +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Rect; +import android.os.Build; +import android.util.Log; +import android.util.Pair; + +import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.keyboard.KeyboardId; +import com.android.inputmethod.keyboard.KeyboardLayoutSet; +import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.settings.Settings; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +final class EmojiCategory { + private final String TAG = EmojiCategory.class.getSimpleName(); + + private static final int ID_UNSPECIFIED = -1; + public static final int ID_RECENTS = 0; + private static final int ID_PEOPLE = 1; + private static final int ID_OBJECTS = 2; + private static final int ID_NATURE = 3; + private static final int ID_PLACES = 4; + private static final int ID_SYMBOLS = 5; + private static final int ID_EMOTICONS = 6; + + public final class CategoryProperties { + public final int mCategoryId; + public final int mPageCount; + public CategoryProperties(final int categoryId, final int pageCount) { + mCategoryId = categoryId; + mPageCount = pageCount; + } + } + + private static final String[] sCategoryName = { + "recents", + "people", + "objects", + "nature", + "places", + "symbols", + "emoticons" }; + + private static final int[] sCategoryTabIconAttr = { + R.styleable.EmojiPalettesView_iconEmojiRecentsTab, + R.styleable.EmojiPalettesView_iconEmojiCategory1Tab, + R.styleable.EmojiPalettesView_iconEmojiCategory2Tab, + R.styleable.EmojiPalettesView_iconEmojiCategory3Tab, + R.styleable.EmojiPalettesView_iconEmojiCategory4Tab, + R.styleable.EmojiPalettesView_iconEmojiCategory5Tab, + R.styleable.EmojiPalettesView_iconEmojiCategory6Tab }; + + private static final int[] sAccessibilityDescriptionResourceIdsForCategories = { + R.string.spoken_descrption_emoji_category_recents, + R.string.spoken_descrption_emoji_category_people, + R.string.spoken_descrption_emoji_category_objects, + R.string.spoken_descrption_emoji_category_nature, + R.string.spoken_descrption_emoji_category_places, + R.string.spoken_descrption_emoji_category_symbols, + R.string.spoken_descrption_emoji_category_emoticons }; + + private static final int[] sCategoryElementId = { + KeyboardId.ELEMENT_EMOJI_RECENTS, + KeyboardId.ELEMENT_EMOJI_CATEGORY1, + KeyboardId.ELEMENT_EMOJI_CATEGORY2, + KeyboardId.ELEMENT_EMOJI_CATEGORY3, + KeyboardId.ELEMENT_EMOJI_CATEGORY4, + KeyboardId.ELEMENT_EMOJI_CATEGORY5, + KeyboardId.ELEMENT_EMOJI_CATEGORY6 }; + + private final SharedPreferences mPrefs; + private final Resources mRes; + private final int mMaxPageKeyCount; + private final KeyboardLayoutSet mLayoutSet; + private final HashMap<String, Integer> mCategoryNameToIdMap = new HashMap<>(); + private final int[] mCategoryTabIconId = new int[sCategoryName.length]; + private final ArrayList<CategoryProperties> mShownCategories = new ArrayList<>(); + private final ConcurrentHashMap<Long, DynamicGridKeyboard> mCategoryKeyboardMap = + new ConcurrentHashMap<>(); + + private int mCurrentCategoryId = EmojiCategory.ID_UNSPECIFIED; + private int mCurrentCategoryPageId = 0; + + public EmojiCategory(final SharedPreferences prefs, final Resources res, + final KeyboardLayoutSet layoutSet, final TypedArray emojiPaletteViewAttr) { + mPrefs = prefs; + mRes = res; + mMaxPageKeyCount = res.getInteger(R.integer.config_emoji_keyboard_max_page_key_count); + mLayoutSet = layoutSet; + for (int i = 0; i < sCategoryName.length; ++i) { + mCategoryNameToIdMap.put(sCategoryName[i], i); + mCategoryTabIconId[i] = emojiPaletteViewAttr.getResourceId( + sCategoryTabIconAttr[i], 0); + } + addShownCategoryId(EmojiCategory.ID_RECENTS); + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2 + || android.os.Build.VERSION.CODENAME.equalsIgnoreCase("KeyLimePie") + || android.os.Build.VERSION.CODENAME.equalsIgnoreCase("KitKat")) { + addShownCategoryId(EmojiCategory.ID_PEOPLE); + addShownCategoryId(EmojiCategory.ID_OBJECTS); + addShownCategoryId(EmojiCategory.ID_NATURE); + addShownCategoryId(EmojiCategory.ID_PLACES); + mCurrentCategoryId = + Settings.readLastShownEmojiCategoryId(mPrefs, EmojiCategory.ID_PEOPLE); + } else { + mCurrentCategoryId = + Settings.readLastShownEmojiCategoryId(mPrefs, EmojiCategory.ID_SYMBOLS); + } + addShownCategoryId(EmojiCategory.ID_SYMBOLS); + addShownCategoryId(EmojiCategory.ID_EMOTICONS); + getKeyboard(EmojiCategory.ID_RECENTS, 0 /* cagetoryPageId */) + .loadRecentKeys(mCategoryKeyboardMap.values()); + } + + private void addShownCategoryId(final int categoryId) { + // Load a keyboard of categoryId + getKeyboard(categoryId, 0 /* cagetoryPageId */); + final CategoryProperties properties = + new CategoryProperties(categoryId, getCategoryPageCount(categoryId)); + mShownCategories.add(properties); + } + + public String getCategoryName(final int categoryId, final int categoryPageId) { + return sCategoryName[categoryId] + "-" + categoryPageId; + } + + public int getCategoryId(final String name) { + final String[] strings = name.split("-"); + return mCategoryNameToIdMap.get(strings[0]); + } + + public int getCategoryTabIcon(final int categoryId) { + return mCategoryTabIconId[categoryId]; + } + + public String getAccessibilityDescription(final int categoryId) { + return mRes.getString(sAccessibilityDescriptionResourceIdsForCategories[categoryId]); + } + + public ArrayList<CategoryProperties> getShownCategories() { + return mShownCategories; + } + + public int getCurrentCategoryId() { + return mCurrentCategoryId; + } + + public int getCurrentCategoryPageSize() { + return getCategoryPageSize(mCurrentCategoryId); + } + + public int getCategoryPageSize(final int categoryId) { + for (final CategoryProperties prop : mShownCategories) { + if (prop.mCategoryId == categoryId) { + return prop.mPageCount; + } + } + Log.w(TAG, "Invalid category id: " + categoryId); + // Should not reach here. + return 0; + } + + public void setCurrentCategoryId(final int categoryId) { + mCurrentCategoryId = categoryId; + Settings.writeLastShownEmojiCategoryId(mPrefs, categoryId); + } + + public void setCurrentCategoryPageId(final int id) { + mCurrentCategoryPageId = id; + } + + public int getCurrentCategoryPageId() { + return mCurrentCategoryPageId; + } + + public void saveLastTypedCategoryPage() { + Settings.writeLastTypedEmojiCategoryPageId( + mPrefs, mCurrentCategoryId, mCurrentCategoryPageId); + } + + public boolean isInRecentTab() { + return mCurrentCategoryId == EmojiCategory.ID_RECENTS; + } + + public int getTabIdFromCategoryId(final int categoryId) { + for (int i = 0; i < mShownCategories.size(); ++i) { + if (mShownCategories.get(i).mCategoryId == categoryId) { + return i; + } + } + Log.w(TAG, "categoryId not found: " + categoryId); + return 0; + } + + // Returns the view pager's page position for the categoryId + public int getPageIdFromCategoryId(final int categoryId) { + final int lastSavedCategoryPageId = + Settings.readLastTypedEmojiCategoryPageId(mPrefs, categoryId); + int sum = 0; + for (int i = 0; i < mShownCategories.size(); ++i) { + final CategoryProperties props = mShownCategories.get(i); + if (props.mCategoryId == categoryId) { + return sum + lastSavedCategoryPageId; + } + sum += props.mPageCount; + } + Log.w(TAG, "categoryId not found: " + categoryId); + return 0; + } + + public int getRecentTabId() { + return getTabIdFromCategoryId(EmojiCategory.ID_RECENTS); + } + + private int getCategoryPageCount(final int categoryId) { + final Keyboard keyboard = mLayoutSet.getKeyboard(sCategoryElementId[categoryId]); + return (keyboard.getSortedKeys().size() - 1) / mMaxPageKeyCount + 1; + } + + // Returns a pair of the category id and the category page id from the view pager's page + // position. The category page id is numbered in each category. And the view page position + // is the position of the current shown page in the view pager which contains all pages of + // all categories. + public Pair<Integer, Integer> getCategoryIdAndPageIdFromPagePosition(final int position) { + int sum = 0; + for (final CategoryProperties properties : mShownCategories) { + final int temp = sum; + sum += properties.mPageCount; + if (sum > position) { + return new Pair<>(properties.mCategoryId, position - temp); + } + } + return null; + } + + // Returns a keyboard from the view pager's page position. + public DynamicGridKeyboard getKeyboardFromPagePosition(final int position) { + final Pair<Integer, Integer> categoryAndId = + getCategoryIdAndPageIdFromPagePosition(position); + if (categoryAndId != null) { + return getKeyboard(categoryAndId.first, categoryAndId.second); + } + return null; + } + + private static final Long getCategoryKeyboardMapKey(final int categoryId, final int id) { + return (((long) categoryId) << Constants.MAX_INT_BIT_COUNT) | id; + } + + public DynamicGridKeyboard getKeyboard(final int categoryId, final int id) { + synchronized (mCategoryKeyboardMap) { + final Long categotyKeyboardMapKey = getCategoryKeyboardMapKey(categoryId, id); + if (mCategoryKeyboardMap.containsKey(categotyKeyboardMapKey)) { + return mCategoryKeyboardMap.get(categotyKeyboardMapKey); + } + + if (categoryId == EmojiCategory.ID_RECENTS) { + final DynamicGridKeyboard kbd = new DynamicGridKeyboard(mPrefs, + mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS), + mMaxPageKeyCount, categoryId); + mCategoryKeyboardMap.put(categotyKeyboardMapKey, kbd); + return kbd; + } + + final Keyboard keyboard = mLayoutSet.getKeyboard(sCategoryElementId[categoryId]); + final Key[][] sortedKeys = sortKeysIntoPages( + keyboard.getSortedKeys(), mMaxPageKeyCount); + for (int pageId = 0; pageId < sortedKeys.length; ++pageId) { + final DynamicGridKeyboard tempKeyboard = new DynamicGridKeyboard(mPrefs, + mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS), + mMaxPageKeyCount, categoryId); + for (final Key emojiKey : sortedKeys[pageId]) { + if (emojiKey == null) { + break; + } + tempKeyboard.addKeyLast(emojiKey); + } + mCategoryKeyboardMap.put( + getCategoryKeyboardMapKey(categoryId, pageId), tempKeyboard); + } + return mCategoryKeyboardMap.get(categotyKeyboardMapKey); + } + } + + public int getTotalPageCountOfAllCategories() { + int sum = 0; + for (CategoryProperties properties : mShownCategories) { + sum += properties.mPageCount; + } + return sum; + } + + private static Comparator<Key> EMOJI_KEY_COMPARATOR = new Comparator<Key>() { + @Override + public int compare(final Key lhs, final Key rhs) { + final Rect lHitBox = lhs.getHitBox(); + final Rect rHitBox = rhs.getHitBox(); + if (lHitBox.top < rHitBox.top) { + return -1; + } else if (lHitBox.top > rHitBox.top) { + return 1; + } + if (lHitBox.left < rHitBox.left) { + return -1; + } else if (lHitBox.left > rHitBox.left) { + return 1; + } + if (lhs.getCode() == rhs.getCode()) { + return 0; + } + return lhs.getCode() < rhs.getCode() ? -1 : 1; + } + }; + + private static Key[][] sortKeysIntoPages(final List<Key> inKeys, final int maxPageCount) { + final ArrayList<Key> keys = new ArrayList<>(inKeys); + Collections.sort(keys, EMOJI_KEY_COMPARATOR); + final int pageCount = (keys.size() - 1) / maxPageCount + 1; + final Key[][] retval = new Key[pageCount][maxPageCount]; + for (int i = 0; i < keys.size(); ++i) { + retval[i / maxPageCount][i % maxPageCount] = keys.get(i); + } + return retval; + } +} diff --git a/java/src/com/android/inputmethod/keyboard/EmojiCategoryPageIndicatorView.java b/java/src/com/android/inputmethod/keyboard/emoji/EmojiCategoryPageIndicatorView.java index e23131a30..43d62c71a 100644 --- a/java/src/com/android/inputmethod/keyboard/EmojiCategoryPageIndicatorView.java +++ b/java/src/com/android/inputmethod/keyboard/emoji/EmojiCategoryPageIndicatorView.java @@ -14,34 +14,36 @@ * limitations under the License. */ -package com.android.inputmethod.keyboard; - -import com.android.inputmethod.latin.R; +package com.android.inputmethod.keyboard.emoji; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.util.AttributeSet; -import android.widget.LinearLayout; +import android.view.View; -public class EmojiCategoryPageIndicatorView extends LinearLayout { - private static final float BOTTOM_MARGIN_RATIO = 0.66f; +public final class EmojiCategoryPageIndicatorView extends View { + private static final float BOTTOM_MARGIN_RATIO = 1.0f; private final Paint mPaint = new Paint(); private int mCategoryPageSize = 0; private int mCurrentCategoryPageId = 0; private float mOffset = 0.0f; - public EmojiCategoryPageIndicatorView(Context context) { - this(context, null /* attrs */); + public EmojiCategoryPageIndicatorView(final Context context, final AttributeSet attrs) { + this(context, attrs, 0); + } + + public EmojiCategoryPageIndicatorView(final Context context, final AttributeSet attrs, + final int defStyle) { + super(context, attrs, defStyle); } - public EmojiCategoryPageIndicatorView(Context context, AttributeSet attrs) { - super(context, attrs); - mPaint.setColor(context.getResources().getColor( - R.color.emoji_category_page_id_view_foreground)); + public void setColors(final int foregroundColor, final int backgroundColor) { + mPaint.setColor(foregroundColor); + setBackgroundColor(backgroundColor); } - public void setCategoryPageId(int size, int id, float offset) { + public void setCategoryPageId(final int size, final int id, final float offset) { mCategoryPageSize = size; mCurrentCategoryPageId = id; mOffset = offset; @@ -49,7 +51,7 @@ public class EmojiCategoryPageIndicatorView extends LinearLayout { } @Override - protected void onDraw(Canvas canvas) { + protected void onDraw(final Canvas canvas) { if (mCategoryPageSize <= 1) { // If the category is not set yet or contains only one category, // just clear and return. diff --git a/java/src/com/android/inputmethod/keyboard/EmojiLayoutParams.java b/java/src/com/android/inputmethod/keyboard/emoji/EmojiLayoutParams.java index 967448c28..582e09124 100644 --- a/java/src/com/android/inputmethod/keyboard/EmojiLayoutParams.java +++ b/java/src/com/android/inputmethod/keyboard/emoji/EmojiLayoutParams.java @@ -14,17 +14,17 @@ * limitations under the License. */ -package com.android.inputmethod.keyboard; - -import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.utils.ResourceUtils; +package com.android.inputmethod.keyboard.emoji; import android.content.res.Resources; import android.support.v4.view.ViewPager; -import android.widget.ImageView; +import android.view.View; import android.widget.LinearLayout; -public class EmojiLayoutParams { +import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.utils.ResourceUtils; + +final class EmojiLayoutParams { private static final int DEFAULT_KEYBOARD_ROWS = 4; public final int mEmojiPagerHeight; @@ -37,22 +37,22 @@ public class EmojiLayoutParams { private final int mBottomPadding; private final int mTopPadding; - public EmojiLayoutParams(Resources res) { + public EmojiLayoutParams(final Resources res) { final int defaultKeyboardHeight = ResourceUtils.getDefaultKeyboardHeight(res); final int defaultKeyboardWidth = ResourceUtils.getDefaultKeyboardWidth(res); - mKeyVerticalGap = (int) res.getFraction(R.fraction.key_bottom_gap_holo, - (int) defaultKeyboardHeight, (int) defaultKeyboardHeight); - mBottomPadding = (int) res.getFraction(R.fraction.keyboard_bottom_padding_holo, - (int) defaultKeyboardHeight, (int) defaultKeyboardHeight); - mTopPadding = (int) res.getFraction(R.fraction.keyboard_top_padding_holo, - (int) defaultKeyboardHeight, (int) defaultKeyboardHeight); - mKeyHorizontalGap = (int) (res.getFraction(R.fraction.key_horizontal_gap_holo, + mKeyVerticalGap = (int) res.getFraction(R.fraction.config_key_vertical_gap_holo, + defaultKeyboardHeight, defaultKeyboardHeight); + mBottomPadding = (int) res.getFraction(R.fraction.config_keyboard_bottom_padding_holo, + defaultKeyboardHeight, defaultKeyboardHeight); + mTopPadding = (int) res.getFraction(R.fraction.config_keyboard_top_padding_holo, + defaultKeyboardHeight, defaultKeyboardHeight); + mKeyHorizontalGap = (int) (res.getFraction(R.fraction.config_key_horizontal_gap_holo, defaultKeyboardWidth, defaultKeyboardWidth)); mEmojiCategoryPageIdViewHeight = - (int) (res.getDimension(R.dimen.emoji_category_page_id_height)); + (int) (res.getDimension(R.dimen.config_emoji_category_page_id_height)); final int baseheight = defaultKeyboardHeight - mBottomPadding - mTopPadding + mKeyVerticalGap; - mEmojiActionBarHeight = ((int) baseheight) / DEFAULT_KEYBOARD_ROWS + mEmojiActionBarHeight = baseheight / DEFAULT_KEYBOARD_ROWS - (mKeyVerticalGap - mBottomPadding) / 2; mEmojiPagerHeight = defaultKeyboardHeight - mEmojiActionBarHeight - mEmojiCategoryPageIdViewHeight; @@ -60,29 +60,33 @@ public class EmojiLayoutParams { mEmojiKeyboardHeight = mEmojiPagerHeight - mEmojiPagerBottomMargin - 1; } - public void setPagerProperties(ViewPager vp) { + public void setPagerProperties(final ViewPager vp) { final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) vp.getLayoutParams(); lp.height = mEmojiKeyboardHeight; lp.bottomMargin = mEmojiPagerBottomMargin; vp.setLayoutParams(lp); } - public void setCategoryPageIdViewProperties(LinearLayout ll) { - final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) ll.getLayoutParams(); + public void setCategoryPageIdViewProperties(final View v) { + final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) v.getLayoutParams(); lp.height = mEmojiCategoryPageIdViewHeight; - ll.setLayoutParams(lp); + v.setLayoutParams(lp); + } + + public int getActionBarHeight() { + return mEmojiActionBarHeight - mBottomPadding; } - public void setActionBarProperties(LinearLayout ll) { + public void setActionBarProperties(final LinearLayout ll) { final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) ll.getLayoutParams(); - lp.height = mEmojiActionBarHeight - mBottomPadding; + lp.height = getActionBarHeight(); ll.setLayoutParams(lp); } - public void setKeyProperties(ImageView ib) { - final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) ib.getLayoutParams(); + public void setKeyProperties(final View v) { + final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) v.getLayoutParams(); lp.leftMargin = mKeyHorizontalGap / 2; lp.rightMargin = mKeyHorizontalGap / 2; - ib.setLayoutParams(lp); + v.setLayoutParams(lp); } } diff --git a/java/src/com/android/inputmethod/keyboard/emoji/EmojiPageKeyboardView.java b/java/src/com/android/inputmethod/keyboard/emoji/EmojiPageKeyboardView.java new file mode 100644 index 000000000..17dfc9cce --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/emoji/EmojiPageKeyboardView.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.emoji; + +import android.content.Context; +import android.os.Handler; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.accessibility.AccessibilityEvent; + +import com.android.inputmethod.accessibility.AccessibilityUtils; +import com.android.inputmethod.accessibility.KeyboardAccessibilityDelegate; +import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.keyboard.KeyDetector; +import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.keyboard.KeyboardView; +import com.android.inputmethod.latin.R; + +/** + * This is an extended {@link KeyboardView} class that hosts an emoji page keyboard. + * Multi-touch unsupported. No gesture support. + */ +// TODO: Implement key popup preview. +final class EmojiPageKeyboardView extends KeyboardView implements + GestureDetector.OnGestureListener { + private static final long KEY_PRESS_DELAY_TIME = 250; // msec + private static final long KEY_RELEASE_DELAY_TIME = 30; // msec + + public interface OnKeyEventListener { + public void onPressKey(Key key); + public void onReleaseKey(Key key); + } + + private static final OnKeyEventListener EMPTY_LISTENER = new OnKeyEventListener() { + @Override + public void onPressKey(final Key key) {} + @Override + public void onReleaseKey(final Key key) {} + }; + + private OnKeyEventListener mListener = EMPTY_LISTENER; + private final KeyDetector mKeyDetector = new KeyDetector(); + private final GestureDetector mGestureDetector; + private KeyboardAccessibilityDelegate<EmojiPageKeyboardView> mAccessibilityDelegate; + + public EmojiPageKeyboardView(final Context context, final AttributeSet attrs) { + this(context, attrs, R.attr.keyboardViewStyle); + } + + public EmojiPageKeyboardView(final Context context, final AttributeSet attrs, + final int defStyle) { + super(context, attrs, defStyle); + mGestureDetector = new GestureDetector(context, this); + mGestureDetector.setIsLongpressEnabled(false /* isLongpressEnabled */); + mHandler = new Handler(); + } + + public void setOnKeyEventListener(final OnKeyEventListener listener) { + mListener = listener; + } + + /** + * {@inheritDoc} + */ + @Override + public void setKeyboard(final Keyboard keyboard) { + super.setKeyboard(keyboard); + mKeyDetector.setKeyboard(keyboard, 0 /* correctionX */, 0 /* correctionY */); + if (AccessibilityUtils.getInstance().isAccessibilityEnabled()) { + if (mAccessibilityDelegate == null) { + mAccessibilityDelegate = new KeyboardAccessibilityDelegate<>(this, mKeyDetector); + } + mAccessibilityDelegate.setKeyboard(keyboard); + } else { + mAccessibilityDelegate = null; + } + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(final AccessibilityEvent event) { + // Don't populate accessibility event with all Emoji keys. + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean onHoverEvent(final MotionEvent event) { + final KeyboardAccessibilityDelegate<EmojiPageKeyboardView> accessibilityDelegate = + mAccessibilityDelegate; + if (accessibilityDelegate != null) { + return accessibilityDelegate.onHoverEvent(event); + } + return super.onHoverEvent(event); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean onTouchEvent(final MotionEvent e) { + if (mGestureDetector.onTouchEvent(e)) { + return true; + } + final Key key = getKey(e); + if (key != null && key != mCurrentKey) { + releaseCurrentKey(false /* withKeyRegistering */); + } + return true; + } + + // {@link GestureEnabler#OnGestureListener} methods. + private Key mCurrentKey; + private Runnable mPendingKeyDown; + private final Handler mHandler; + + private Key getKey(final MotionEvent e) { + final int index = e.getActionIndex(); + final int x = (int)e.getX(index); + final int y = (int)e.getY(index); + return mKeyDetector.detectHitKey(x, y); + } + + public void releaseCurrentKey(final boolean withKeyRegistering) { + mHandler.removeCallbacks(mPendingKeyDown); + mPendingKeyDown = null; + final Key currentKey = mCurrentKey; + if (currentKey == null) { + return; + } + currentKey.onReleased(); + invalidateKey(currentKey); + if (withKeyRegistering) { + mListener.onReleaseKey(currentKey); + } + mCurrentKey = null; + } + + @Override + public boolean onDown(final MotionEvent e) { + final Key key = getKey(e); + releaseCurrentKey(false /* withKeyRegistering */); + mCurrentKey = key; + if (key == null) { + return false; + } + // Do not trigger key-down effect right now in case this is actually a fling action. + mPendingKeyDown = new Runnable() { + @Override + public void run() { + mPendingKeyDown = null; + key.onPressed(); + invalidateKey(key); + mListener.onPressKey(key); + } + }; + mHandler.postDelayed(mPendingKeyDown, KEY_PRESS_DELAY_TIME); + return false; + } + + @Override + public void onShowPress(final MotionEvent e) { + // User feedback is done at {@link #onDown(MotionEvent)}. + } + + @Override + public boolean onSingleTapUp(final MotionEvent e) { + final Key key = getKey(e); + final Runnable pendingKeyDown = mPendingKeyDown; + final Key currentKey = mCurrentKey; + releaseCurrentKey(false /* withKeyRegistering */); + if (key == null) { + return false; + } + if (key == currentKey && pendingKeyDown != null) { + pendingKeyDown.run(); + // Trigger key-release event a little later so that a user can see visual feedback. + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + key.onReleased(); + invalidateKey(key); + mListener.onReleaseKey(key); + } + }, KEY_RELEASE_DELAY_TIME); + } else { + key.onReleased(); + invalidateKey(key); + mListener.onReleaseKey(key); + } + return true; + } + + @Override + public boolean onScroll(final MotionEvent e1, final MotionEvent e2, final float distanceX, + final float distanceY) { + releaseCurrentKey(false /* withKeyRegistering */); + return false; + } + + @Override + public boolean onFling(final MotionEvent e1, final MotionEvent e2, final float velocityX, + final float velocityY) { + releaseCurrentKey(false /* withKeyRegistering */); + return false; + } + + @Override + public void onLongPress(final MotionEvent e) { + // Long press detection of {@link #mGestureDetector} is disabled and not used. + } +} diff --git a/java/src/com/android/inputmethod/keyboard/emoji/EmojiPalettesAdapter.java b/java/src/com/android/inputmethod/keyboard/emoji/EmojiPalettesAdapter.java new file mode 100644 index 000000000..68056e0eb --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/emoji/EmojiPalettesAdapter.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.emoji; + +import android.support.v4.view.PagerAdapter; +import android.util.Log; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.keyboard.KeyboardView; +import com.android.inputmethod.latin.R; + +final class EmojiPalettesAdapter extends PagerAdapter { + private static final String TAG = EmojiPalettesAdapter.class.getSimpleName(); + private static final boolean DEBUG_PAGER = false; + + private final EmojiPageKeyboardView.OnKeyEventListener mListener; + private final DynamicGridKeyboard mRecentsKeyboard; + private final SparseArray<EmojiPageKeyboardView> mActiveKeyboardViews = new SparseArray<>(); + private final EmojiCategory mEmojiCategory; + private int mActivePosition = 0; + + public EmojiPalettesAdapter(final EmojiCategory emojiCategory, + final EmojiPageKeyboardView.OnKeyEventListener listener) { + mEmojiCategory = emojiCategory; + mListener = listener; + mRecentsKeyboard = mEmojiCategory.getKeyboard(EmojiCategory.ID_RECENTS, 0); + } + + public void flushPendingRecentKeys() { + mRecentsKeyboard.flushPendingRecentKeys(); + final KeyboardView recentKeyboardView = + mActiveKeyboardViews.get(mEmojiCategory.getRecentTabId()); + if (recentKeyboardView != null) { + recentKeyboardView.invalidateAllKeys(); + } + } + + public void addRecentKey(final Key key) { + if (mEmojiCategory.isInRecentTab()) { + mRecentsKeyboard.addPendingKey(key); + return; + } + mRecentsKeyboard.addKeyFirst(key); + final KeyboardView recentKeyboardView = + mActiveKeyboardViews.get(mEmojiCategory.getRecentTabId()); + if (recentKeyboardView != null) { + recentKeyboardView.invalidateAllKeys(); + } + } + + public void onPageScrolled() { + releaseCurrentKey(false /* withKeyRegistering */); + } + + public void releaseCurrentKey(final boolean withKeyRegistering) { + // Make sure the delayed key-down event (highlight effect and haptic feedback) will be + // canceled. + final EmojiPageKeyboardView currentKeyboardView = + mActiveKeyboardViews.get(mActivePosition); + if (currentKeyboardView == null) { + return; + } + currentKeyboardView.releaseCurrentKey(withKeyRegistering); + } + + @Override + public int getCount() { + return mEmojiCategory.getTotalPageCountOfAllCategories(); + } + + @Override + public void setPrimaryItem(final ViewGroup container, final int position, + final Object object) { + if (mActivePosition == position) { + return; + } + final EmojiPageKeyboardView oldKeyboardView = mActiveKeyboardViews.get(mActivePosition); + if (oldKeyboardView != null) { + oldKeyboardView.releaseCurrentKey(false /* withKeyRegistering */); + oldKeyboardView.deallocateMemory(); + } + mActivePosition = position; + } + + @Override + public Object instantiateItem(final ViewGroup container, final int position) { + if (DEBUG_PAGER) { + Log.d(TAG, "instantiate item: " + position); + } + final EmojiPageKeyboardView oldKeyboardView = mActiveKeyboardViews.get(position); + if (oldKeyboardView != null) { + oldKeyboardView.deallocateMemory(); + // This may be redundant but wanted to be safer.. + mActiveKeyboardViews.remove(position); + } + final Keyboard keyboard = + mEmojiCategory.getKeyboardFromPagePosition(position); + final LayoutInflater inflater = LayoutInflater.from(container.getContext()); + final EmojiPageKeyboardView keyboardView = (EmojiPageKeyboardView)inflater.inflate( + R.layout.emoji_keyboard_page, container, false /* attachToRoot */); + keyboardView.setKeyboard(keyboard); + keyboardView.setOnKeyEventListener(mListener); + container.addView(keyboardView); + mActiveKeyboardViews.put(position, keyboardView); + return keyboardView; + } + + @Override + public boolean isViewFromObject(final View view, final Object object) { + return view == object; + } + + @Override + public void destroyItem(final ViewGroup container, final int position, + final Object object) { + if (DEBUG_PAGER) { + Log.d(TAG, "destroy item: " + position + ", " + object.getClass().getSimpleName()); + } + final EmojiPageKeyboardView keyboardView = mActiveKeyboardViews.get(position); + if (keyboardView != null) { + keyboardView.deallocateMemory(); + mActiveKeyboardViews.remove(position); + } + if (object instanceof View) { + container.removeView((View)object); + } else { + Log.w(TAG, "Warning!!! Emoji palette may be leaking. " + object); + } + } +} diff --git a/java/src/com/android/inputmethod/keyboard/emoji/EmojiPalettesView.java b/java/src/com/android/inputmethod/keyboard/emoji/EmojiPalettesView.java new file mode 100644 index 000000000..e37cd2369 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/emoji/EmojiPalettesView.java @@ -0,0 +1,560 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.emoji; + +import static com.android.inputmethod.latin.Constants.NOT_A_COORDINATE; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.os.CountDownTimer; +import android.preference.PreferenceManager; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.util.Pair; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TabHost; +import android.widget.TabHost.OnTabChangeListener; +import android.widget.TabWidget; +import android.widget.TextView; + +import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.keyboard.KeyboardActionListener; +import com.android.inputmethod.keyboard.KeyboardLayoutSet; +import com.android.inputmethod.keyboard.KeyboardView; +import com.android.inputmethod.keyboard.internal.KeyDrawParams; +import com.android.inputmethod.keyboard.internal.KeyVisualAttributes; +import com.android.inputmethod.keyboard.internal.KeyboardIconsSet; +import com.android.inputmethod.latin.AudioAndHapticFeedbackManager; +import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.SubtypeSwitcher; +import com.android.inputmethod.latin.utils.ResourceUtils; + +import java.util.concurrent.TimeUnit; + +/** + * View class to implement Emoji palettes. + * The Emoji keyboard consists of group of views layout/emoji_palettes_view. + * <ol> + * <li> Emoji category tabs. + * <li> Delete button. + * <li> Emoji keyboard pages that can be scrolled by swiping horizontally or by selecting a tab. + * <li> Back to main keyboard button and enter button. + * </ol> + * Because of the above reasons, this class doesn't extend {@link KeyboardView}. + */ +public final class EmojiPalettesView extends LinearLayout implements OnTabChangeListener, + ViewPager.OnPageChangeListener, View.OnClickListener, View.OnTouchListener, + EmojiPageKeyboardView.OnKeyEventListener { + private final int mFunctionalKeyBackgroundId; + private final int mSpacebarBackgroundId; + private final boolean mCategoryIndicatorEnabled; + private final int mCategoryIndicatorDrawableResId; + private final int mCategoryIndicatorBackgroundResId; + private final int mCategoryPageIndicatorColor; + private final int mCategoryPageIndicatorBackground; + private final DeleteKeyOnTouchListener mDeleteKeyOnTouchListener; + private EmojiPalettesAdapter mEmojiPalettesAdapter; + private final EmojiLayoutParams mEmojiLayoutParams; + + private ImageButton mDeleteKey; + private TextView mAlphabetKeyLeft; + private TextView mAlphabetKeyRight; + private View mSpacebar; + // TODO: Remove this workaround. + private View mSpacebarIcon; + private TabHost mTabHost; + private ViewPager mEmojiPager; + private int mCurrentPagerPosition = 0; + private EmojiCategoryPageIndicatorView mEmojiCategoryPageIndicatorView; + + private KeyboardActionListener mKeyboardActionListener = KeyboardActionListener.EMPTY_LISTENER; + + private final EmojiCategory mEmojiCategory; + + public EmojiPalettesView(final Context context, final AttributeSet attrs) { + this(context, attrs, R.attr.emojiPalettesViewStyle); + } + + public EmojiPalettesView(final Context context, final AttributeSet attrs, final int defStyle) { + super(context, attrs, defStyle); + final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs, + R.styleable.KeyboardView, defStyle, R.style.KeyboardView); + final int keyBackgroundId = keyboardViewAttr.getResourceId( + R.styleable.KeyboardView_keyBackground, 0); + mFunctionalKeyBackgroundId = keyboardViewAttr.getResourceId( + R.styleable.KeyboardView_functionalKeyBackground, keyBackgroundId); + mSpacebarBackgroundId = keyboardViewAttr.getResourceId( + R.styleable.KeyboardView_spacebarBackground, keyBackgroundId); + keyboardViewAttr.recycle(); + final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder( + context, null /* editorInfo */); + final Resources res = context.getResources(); + mEmojiLayoutParams = new EmojiLayoutParams(res); + builder.setSubtype(SubtypeSwitcher.getInstance().getEmojiSubtype()); + builder.setKeyboardGeometry(ResourceUtils.getDefaultKeyboardWidth(res), + mEmojiLayoutParams.mEmojiKeyboardHeight); + final KeyboardLayoutSet layoutSet = builder.build(); + final TypedArray emojiPalettesViewAttr = context.obtainStyledAttributes(attrs, + R.styleable.EmojiPalettesView, defStyle, R.style.EmojiPalettesView); + mEmojiCategory = new EmojiCategory(PreferenceManager.getDefaultSharedPreferences(context), + res, layoutSet, emojiPalettesViewAttr); + mCategoryIndicatorEnabled = emojiPalettesViewAttr.getBoolean( + R.styleable.EmojiPalettesView_categoryIndicatorEnabled, false); + mCategoryIndicatorDrawableResId = emojiPalettesViewAttr.getResourceId( + R.styleable.EmojiPalettesView_categoryIndicatorDrawable, 0); + mCategoryIndicatorBackgroundResId = emojiPalettesViewAttr.getResourceId( + R.styleable.EmojiPalettesView_categoryIndicatorBackground, 0); + mCategoryPageIndicatorColor = emojiPalettesViewAttr.getColor( + R.styleable.EmojiPalettesView_categoryPageIndicatorColor, 0); + mCategoryPageIndicatorBackground = emojiPalettesViewAttr.getColor( + R.styleable.EmojiPalettesView_categoryPageIndicatorBackground, 0); + emojiPalettesViewAttr.recycle(); + mDeleteKeyOnTouchListener = new DeleteKeyOnTouchListener(context); + } + + @Override + protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + final Resources res = getContext().getResources(); + // The main keyboard expands to the entire this {@link KeyboardView}. + final int width = ResourceUtils.getDefaultKeyboardWidth(res) + + getPaddingLeft() + getPaddingRight(); + final int height = ResourceUtils.getDefaultKeyboardHeight(res) + + res.getDimensionPixelSize(R.dimen.config_suggestions_strip_height) + + getPaddingTop() + getPaddingBottom(); + setMeasuredDimension(width, height); + } + + private void addTab(final TabHost host, final int categoryId) { + final String tabId = mEmojiCategory.getCategoryName(categoryId, 0 /* categoryPageId */); + final TabHost.TabSpec tspec = host.newTabSpec(tabId); + tspec.setContent(R.id.emoji_keyboard_dummy); + final ImageView iconView = (ImageView)LayoutInflater.from(getContext()).inflate( + R.layout.emoji_keyboard_tab_icon, null); + iconView.setImageResource(mEmojiCategory.getCategoryTabIcon(categoryId)); + iconView.setContentDescription(mEmojiCategory.getAccessibilityDescription(categoryId)); + tspec.setIndicator(iconView); + host.addTab(tspec); + } + + @Override + protected void onFinishInflate() { + mTabHost = (TabHost)findViewById(R.id.emoji_category_tabhost); + mTabHost.setup(); + for (final EmojiCategory.CategoryProperties properties + : mEmojiCategory.getShownCategories()) { + addTab(mTabHost, properties.mCategoryId); + } + mTabHost.setOnTabChangedListener(this); + final TabWidget tabWidget = mTabHost.getTabWidget(); + tabWidget.setStripEnabled(mCategoryIndicatorEnabled); + if (mCategoryIndicatorEnabled) { + // On TabWidget's strip, what looks like an indicator is actually a background. + // And what looks like a background are actually left and right drawables. + tabWidget.setBackgroundResource(mCategoryIndicatorDrawableResId); + tabWidget.setLeftStripDrawable(mCategoryIndicatorBackgroundResId); + tabWidget.setRightStripDrawable(mCategoryIndicatorBackgroundResId); + } + + mEmojiPalettesAdapter = new EmojiPalettesAdapter(mEmojiCategory, this); + + mEmojiPager = (ViewPager)findViewById(R.id.emoji_keyboard_pager); + mEmojiPager.setAdapter(mEmojiPalettesAdapter); + mEmojiPager.setOnPageChangeListener(this); + mEmojiPager.setOffscreenPageLimit(0); + mEmojiPager.setPersistentDrawingCache(PERSISTENT_NO_CACHE); + mEmojiLayoutParams.setPagerProperties(mEmojiPager); + + mEmojiCategoryPageIndicatorView = + (EmojiCategoryPageIndicatorView)findViewById(R.id.emoji_category_page_id_view); + mEmojiCategoryPageIndicatorView.setColors( + mCategoryPageIndicatorColor, mCategoryPageIndicatorBackground); + mEmojiLayoutParams.setCategoryPageIdViewProperties(mEmojiCategoryPageIndicatorView); + + setCurrentCategoryId(mEmojiCategory.getCurrentCategoryId(), true /* force */); + + final LinearLayout actionBar = (LinearLayout)findViewById(R.id.emoji_action_bar); + mEmojiLayoutParams.setActionBarProperties(actionBar); + + // deleteKey depends only on OnTouchListener. + mDeleteKey = (ImageButton)findViewById(R.id.emoji_keyboard_delete); + mDeleteKey.setBackgroundResource(mFunctionalKeyBackgroundId); + mDeleteKey.setTag(Constants.CODE_DELETE); + mDeleteKey.setOnTouchListener(mDeleteKeyOnTouchListener); + + // {@link #mAlphabetKeyLeft}, {@link #mAlphabetKeyRight, and spaceKey depend on + // {@link View.OnClickListener} as well as {@link View.OnTouchListener}. + // {@link View.OnTouchListener} is used as the trigger of key-press, while + // {@link View.OnClickListener} is used as the trigger of key-release which does not occur + // if the event is canceled by moving off the finger from the view. + // The text on alphabet keys are set at + // {@link #startEmojiPalettes(String,int,float,Typeface)}. + mAlphabetKeyLeft = (TextView)findViewById(R.id.emoji_keyboard_alphabet_left); + mAlphabetKeyLeft.setBackgroundResource(mFunctionalKeyBackgroundId); + mAlphabetKeyLeft.setTag(Constants.CODE_ALPHA_FROM_EMOJI); + mAlphabetKeyLeft.setOnTouchListener(this); + mAlphabetKeyLeft.setOnClickListener(this); + mAlphabetKeyRight = (TextView)findViewById(R.id.emoji_keyboard_alphabet_right); + mAlphabetKeyRight.setBackgroundResource(mFunctionalKeyBackgroundId); + mAlphabetKeyRight.setTag(Constants.CODE_ALPHA_FROM_EMOJI); + mAlphabetKeyRight.setOnTouchListener(this); + mAlphabetKeyRight.setOnClickListener(this); + mSpacebar = findViewById(R.id.emoji_keyboard_space); + mSpacebar.setBackgroundResource(mSpacebarBackgroundId); + mSpacebar.setTag(Constants.CODE_SPACE); + mSpacebar.setOnTouchListener(this); + mSpacebar.setOnClickListener(this); + mEmojiLayoutParams.setKeyProperties(mSpacebar); + mSpacebarIcon = findViewById(R.id.emoji_keyboard_space_icon); + } + + @Override + public boolean dispatchTouchEvent(final MotionEvent ev) { + // Add here to the stack trace to nail down the {@link IllegalArgumentException} exception + // in MotionEvent that sporadically happens. + // TODO: Remove this override method once the issue has been addressed. + return super.dispatchTouchEvent(ev); + } + + @Override + public void onTabChanged(final String tabId) { + AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback( + Constants.CODE_UNSPECIFIED, this); + final int categoryId = mEmojiCategory.getCategoryId(tabId); + setCurrentCategoryId(categoryId, false /* force */); + updateEmojiCategoryPageIdView(); + } + + @Override + public void onPageSelected(final int position) { + final Pair<Integer, Integer> newPos = + mEmojiCategory.getCategoryIdAndPageIdFromPagePosition(position); + setCurrentCategoryId(newPos.first /* categoryId */, false /* force */); + mEmojiCategory.setCurrentCategoryPageId(newPos.second /* categoryPageId */); + updateEmojiCategoryPageIdView(); + mCurrentPagerPosition = position; + } + + @Override + public void onPageScrollStateChanged(final int state) { + // Ignore this message. Only want the actual page selected. + } + + @Override + public void onPageScrolled(final int position, final float positionOffset, + final int positionOffsetPixels) { + mEmojiPalettesAdapter.onPageScrolled(); + final Pair<Integer, Integer> newPos = + mEmojiCategory.getCategoryIdAndPageIdFromPagePosition(position); + final int newCategoryId = newPos.first; + final int newCategorySize = mEmojiCategory.getCategoryPageSize(newCategoryId); + final int currentCategoryId = mEmojiCategory.getCurrentCategoryId(); + final int currentCategoryPageId = mEmojiCategory.getCurrentCategoryPageId(); + final int currentCategorySize = mEmojiCategory.getCurrentCategoryPageSize(); + if (newCategoryId == currentCategoryId) { + mEmojiCategoryPageIndicatorView.setCategoryPageId( + newCategorySize, newPos.second, positionOffset); + } else if (newCategoryId > currentCategoryId) { + mEmojiCategoryPageIndicatorView.setCategoryPageId( + currentCategorySize, currentCategoryPageId, positionOffset); + } else if (newCategoryId < currentCategoryId) { + mEmojiCategoryPageIndicatorView.setCategoryPageId( + currentCategorySize, currentCategoryPageId, positionOffset - 1); + } + } + + /** + * Called from {@link EmojiPageKeyboardView} through {@link android.view.View.OnTouchListener} + * interface to handle touch events from View-based elements such as the space bar. + * Note that this method is used only for observing {@link MotionEvent#ACTION_DOWN} to trigger + * {@link KeyboardActionListener#onPressKey}. {@link KeyboardActionListener#onReleaseKey} will + * be covered by {@link #onClick} as long as the event is not canceled. + */ + @Override + public boolean onTouch(final View v, final MotionEvent event) { + if (event.getActionMasked() != MotionEvent.ACTION_DOWN) { + return false; + } + final Object tag = v.getTag(); + if (!(tag instanceof Integer)) { + return false; + } + final int code = (Integer) tag; + mKeyboardActionListener.onPressKey( + code, 0 /* repeatCount */, true /* isSinglePointer */); + // It's important to return false here. Otherwise, {@link #onClick} and touch-down visual + // feedback stop working. + return false; + } + + /** + * Called from {@link EmojiPageKeyboardView} through {@link android.view.View.OnClickListener} + * interface to handle non-canceled touch-up events from View-based elements such as the space + * bar. + */ + @Override + public void onClick(View v) { + final Object tag = v.getTag(); + if (!(tag instanceof Integer)) { + return; + } + final int code = (Integer) tag; + mKeyboardActionListener.onCodeInput(code, NOT_A_COORDINATE, NOT_A_COORDINATE, + false /* isKeyRepeat */); + mKeyboardActionListener.onReleaseKey(code, false /* withSliding */); + } + + /** + * Called from {@link EmojiPageKeyboardView} through + * {@link com.android.inputmethod.keyboard.emoji.EmojiPageKeyboardView.OnKeyEventListener} + * interface to handle touch events from non-View-based elements such as Emoji buttons. + */ + @Override + public void onPressKey(final Key key) { + final int code = key.getCode(); + mKeyboardActionListener.onPressKey(code, 0 /* repeatCount */, true /* isSinglePointer */); + } + + /** + * Called from {@link EmojiPageKeyboardView} through + * {@link com.android.inputmethod.keyboard.emoji.EmojiPageKeyboardView.OnKeyEventListener} + * interface to handle touch events from non-View-based elements such as Emoji buttons. + */ + @Override + public void onReleaseKey(final Key key) { + mEmojiPalettesAdapter.addRecentKey(key); + mEmojiCategory.saveLastTypedCategoryPage(); + final int code = key.getCode(); + if (code == Constants.CODE_OUTPUT_TEXT) { + mKeyboardActionListener.onTextInput(key.getOutputText()); + } else { + mKeyboardActionListener.onCodeInput(code, NOT_A_COORDINATE, NOT_A_COORDINATE, + false /* isKeyRepeat */); + } + mKeyboardActionListener.onReleaseKey(code, false /* withSliding */); + } + + public void setHardwareAcceleratedDrawingEnabled(final boolean enabled) { + if (!enabled) return; + // TODO: Should use LAYER_TYPE_SOFTWARE when hardware acceleration is off? + setLayerType(LAYER_TYPE_HARDWARE, null); + } + + private static void setupAlphabetKey(final TextView alphabetKey, final String label, + final KeyDrawParams params) { + alphabetKey.setText(label); + alphabetKey.setTextColor(params.mFunctionalTextColor); + alphabetKey.setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mLabelSize); + alphabetKey.setTypeface(params.mTypeface); + } + + public void startEmojiPalettes(final String switchToAlphaLabel, + final KeyVisualAttributes keyVisualAttr, final KeyboardIconsSet iconSet) { + final int deleteIconResId = iconSet.getIconResourceId(KeyboardIconsSet.NAME_DELETE_KEY); + if (deleteIconResId != 0) { + mDeleteKey.setImageResource(deleteIconResId); + } + final int spacebarResId = iconSet.getIconResourceId(KeyboardIconsSet.NAME_SPACE_KEY); + if (spacebarResId != 0) { + // TODO: Remove this workaround to place the spacebar icon. + mSpacebarIcon.setBackgroundResource(spacebarResId); + } + final KeyDrawParams params = new KeyDrawParams(); + params.updateParams(mEmojiLayoutParams.getActionBarHeight(), keyVisualAttr); + setupAlphabetKey(mAlphabetKeyLeft, switchToAlphaLabel, params); + setupAlphabetKey(mAlphabetKeyRight, switchToAlphaLabel, params); + mEmojiPager.setAdapter(mEmojiPalettesAdapter); + mEmojiPager.setCurrentItem(mCurrentPagerPosition); + } + + public void stopEmojiPalettes() { + mEmojiPalettesAdapter.releaseCurrentKey(true /* withKeyRegistering */); + mEmojiPalettesAdapter.flushPendingRecentKeys(); + mEmojiPager.setAdapter(null); + } + + public void setKeyboardActionListener(final KeyboardActionListener listener) { + mKeyboardActionListener = listener; + mDeleteKeyOnTouchListener.setKeyboardActionListener(mKeyboardActionListener); + } + + private void updateEmojiCategoryPageIdView() { + if (mEmojiCategoryPageIndicatorView == null) { + return; + } + mEmojiCategoryPageIndicatorView.setCategoryPageId( + mEmojiCategory.getCurrentCategoryPageSize(), + mEmojiCategory.getCurrentCategoryPageId(), 0.0f /* offset */); + } + + private void setCurrentCategoryId(final int categoryId, final boolean force) { + final int oldCategoryId = mEmojiCategory.getCurrentCategoryId(); + if (oldCategoryId == categoryId && !force) { + return; + } + + if (oldCategoryId == EmojiCategory.ID_RECENTS) { + // Needs to save pending updates for recent keys when we get out of the recents + // category because we don't want to move the recent emojis around while the user + // is in the recents category. + mEmojiPalettesAdapter.flushPendingRecentKeys(); + } + + mEmojiCategory.setCurrentCategoryId(categoryId); + final int newTabId = mEmojiCategory.getTabIdFromCategoryId(categoryId); + final int newCategoryPageId = mEmojiCategory.getPageIdFromCategoryId(categoryId); + if (force || mEmojiCategory.getCategoryIdAndPageIdFromPagePosition( + mEmojiPager.getCurrentItem()).first != categoryId) { + mEmojiPager.setCurrentItem(newCategoryPageId, false /* smoothScroll */); + } + if (force || mTabHost.getCurrentTab() != newTabId) { + mTabHost.setCurrentTab(newTabId); + } + } + + private static class DeleteKeyOnTouchListener implements OnTouchListener { + static final long MAX_REPEAT_COUNT_TIME = TimeUnit.SECONDS.toMillis(30); + final long mKeyRepeatStartTimeout; + final long mKeyRepeatInterval; + + public DeleteKeyOnTouchListener(Context context) { + final Resources res = context.getResources(); + mKeyRepeatStartTimeout = res.getInteger(R.integer.config_key_repeat_start_timeout); + mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval); + mTimer = new CountDownTimer(MAX_REPEAT_COUNT_TIME, mKeyRepeatInterval) { + @Override + public void onTick(long millisUntilFinished) { + final long elapsed = MAX_REPEAT_COUNT_TIME - millisUntilFinished; + if (elapsed < mKeyRepeatStartTimeout) { + return; + } + onKeyRepeat(); + } + @Override + public void onFinish() { + onKeyRepeat(); + } + }; + } + + /** Key-repeat state. */ + private static final int KEY_REPEAT_STATE_INITIALIZED = 0; + // The key is touched but auto key-repeat is not started yet. + private static final int KEY_REPEAT_STATE_KEY_DOWN = 1; + // At least one key-repeat event has already been triggered and the key is not released. + private static final int KEY_REPEAT_STATE_KEY_REPEAT = 2; + + private KeyboardActionListener mKeyboardActionListener = + KeyboardActionListener.EMPTY_LISTENER; + + // TODO: Do the same things done in PointerTracker + private final CountDownTimer mTimer; + private int mState = KEY_REPEAT_STATE_INITIALIZED; + private int mRepeatCount = 0; + + public void setKeyboardActionListener(final KeyboardActionListener listener) { + mKeyboardActionListener = listener; + } + + @Override + public boolean onTouch(final View v, final MotionEvent event) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + onTouchDown(v); + return true; + case MotionEvent.ACTION_MOVE: + final float x = event.getX(); + final float y = event.getY(); + if (x < 0.0f || v.getWidth() < x || y < 0.0f || v.getHeight() < y) { + // Stop generating key events once the finger moves away from the view area. + onTouchCanceled(v); + } + return true; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + onTouchUp(v); + return true; + } + return false; + } + + private void handleKeyDown() { + mKeyboardActionListener.onPressKey( + Constants.CODE_DELETE, mRepeatCount, true /* isSinglePointer */); + } + + private void handleKeyUp() { + mKeyboardActionListener.onCodeInput(Constants.CODE_DELETE, + NOT_A_COORDINATE, NOT_A_COORDINATE, false /* isKeyRepeat */); + mKeyboardActionListener.onReleaseKey( + Constants.CODE_DELETE, false /* withSliding */); + ++mRepeatCount; + } + + private void onTouchDown(final View v) { + mTimer.cancel(); + mRepeatCount = 0; + handleKeyDown(); + v.setPressed(true /* pressed */); + mState = KEY_REPEAT_STATE_KEY_DOWN; + mTimer.start(); + } + + private void onTouchUp(final View v) { + mTimer.cancel(); + if (mState == KEY_REPEAT_STATE_KEY_DOWN) { + handleKeyUp(); + } + v.setPressed(false /* pressed */); + mState = KEY_REPEAT_STATE_INITIALIZED; + } + + private void onTouchCanceled(final View v) { + mTimer.cancel(); + v.setBackgroundColor(Color.TRANSPARENT); + mState = KEY_REPEAT_STATE_INITIALIZED; + } + + // Called by {@link #mTimer} in the UI thread as an auto key-repeat signal. + void onKeyRepeat() { + switch (mState) { + case KEY_REPEAT_STATE_INITIALIZED: + // Basically this should not happen. + break; + case KEY_REPEAT_STATE_KEY_DOWN: + // Do not call {@link #handleKeyDown} here because it has already been called + // in {@link #onTouchDown}. + handleKeyUp(); + mState = KEY_REPEAT_STATE_KEY_REPEAT; + break; + case KEY_REPEAT_STATE_KEY_REPEAT: + handleKeyDown(); + handleKeyUp(); + break; + } + } + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/AbstractDrawingPreview.java b/java/src/com/android/inputmethod/keyboard/internal/AbstractDrawingPreview.java index b814fc162..a194f3dfd 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/AbstractDrawingPreview.java +++ b/java/src/com/android/inputmethod/keyboard/internal/AbstractDrawingPreview.java @@ -22,36 +22,50 @@ import android.view.View; import com.android.inputmethod.keyboard.PointerTracker; /** - * Abstract base class for previews that are drawn on PreviewPlacerView, e.g., - * GestureFloatingPrevewText, GestureTrail, and SlidingKeyInputPreview. + * Abstract base class for previews that are drawn on DrawingPreviewPlacerView, e.g., + * GestureFloatingTextDrawingPreview, GestureTrailsDrawingPreview, and + * SlidingKeyInputDrawingPreview. */ public abstract class AbstractDrawingPreview { - private final View mDrawingView; + private View mDrawingView; private boolean mPreviewEnabled; + private boolean mHasValidGeometry; - protected AbstractDrawingPreview(final View drawingView) { + public void setDrawingView(final DrawingPreviewPlacerView drawingView) { mDrawingView = drawingView; + drawingView.addPreview(this); } - public final View getDrawingView() { - return mDrawingView; + protected void invalidateDrawingView() { + if (mDrawingView != null) { + mDrawingView.invalidate(); + } } - public final void setPreviewEnabled(final boolean enabled) { - mPreviewEnabled = enabled; + protected final boolean isPreviewEnabled() { + return mPreviewEnabled && mHasValidGeometry; } - public boolean isPreviewEnabled() { - return mPreviewEnabled; + public final void setPreviewEnabled(final boolean enabled) { + mPreviewEnabled = enabled; } - public void setKeyboardGeometry(final int[] originCoords, final int width, final int height) { - // Default implementation is empty. + /** + * Set {@link MainKeyboardView} geometry and position in the {@link SoftInputWindow}. + * The class that is overriding this method must call this super implementation. + * + * @param originCoords the top-left coordinates of the {@link MainKeyboardView} in + * {@link SoftInputWindow} coordinate-system. This is unused but has a point in an + * extended class, such as {@link GestureTrailsDrawingPreview}. + * @param width the width of {@link MainKeyboardView}. + * @param height the height of {@link MainKeyboardView}. + */ + public void setKeyboardViewGeometry(final int[] originCoords, final int width, + final int height) { + mHasValidGeometry = (width > 0 && height > 0); } - public void onDetachFromWindow() { - // Default implementation is empty. - } + public abstract void onDeallocateMemory(); /** * Draws the preview diff --git a/java/src/com/android/inputmethod/keyboard/internal/BatchInputArbiter.java b/java/src/com/android/inputmethod/keyboard/internal/BatchInputArbiter.java new file mode 100644 index 000000000..cd9875955 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/BatchInputArbiter.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.internal; + +import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.InputPointers; + +/** + * This class arbitrates batch input. + * An instance of this class holds a {@link GestureStrokeRecognitionPoints}. + * And it arbitrates multiple strokes gestured by multiple fingers and aggregates those gesture + * points into one batch input. + */ +public class BatchInputArbiter { + public interface BatchInputArbiterListener { + public void onStartBatchInput(); + public void onUpdateBatchInput( + final InputPointers aggregatedPointers, final long moveEventTime); + public void onStartUpdateBatchInputTimer(); + public void onEndBatchInput(final InputPointers aggregatedPointers, final long upEventTime); + } + + // The starting time of the first stroke of a gesture input. + private static long sGestureFirstDownTime; + // The {@link InputPointers} that includes all events of a gesture input. + private static final InputPointers sAggregatedPointers = new InputPointers( + Constants.DEFAULT_GESTURE_POINTS_CAPACITY); + private static int sLastRecognitionPointSize = 0; // synchronized using sAggregatedPointers + private static long sLastRecognitionTime = 0; // synchronized using sAggregatedPointers + + private final GestureStrokeRecognitionPoints mRecognitionPoints; + + public BatchInputArbiter(final int pointerId, final GestureStrokeRecognitionParams params) { + mRecognitionPoints = new GestureStrokeRecognitionPoints(pointerId, params); + } + + public void setKeyboardGeometry(final int keyWidth, final int keyboardHeight) { + mRecognitionPoints.setKeyboardGeometry(keyWidth, keyboardHeight); + } + + /** + * Calculate elapsed time since the first gesture down. + * @param eventTime the time of this event. + * @return the elapsed time in millisecond from the first gesture down. + */ + public int getElapsedTimeSinceFirstDown(final long eventTime) { + return (int)(eventTime - sGestureFirstDownTime); + } + + /** + * Add a down event point. + * @param x the x-coordinate of this down event. + * @param y the y-coordinate of this down event. + * @param downEventTime the time of this down event. + * @param lastLetterTypingTime the last typing input time. + * @param activePointerCount the number of active pointers when this pointer down event occurs. + */ + public void addDownEventPoint(final int x, final int y, final long downEventTime, + final long lastLetterTypingTime, final int activePointerCount) { + if (activePointerCount == 1) { + sGestureFirstDownTime = downEventTime; + } + final int elapsedTimeSinceFirstDown = getElapsedTimeSinceFirstDown(downEventTime); + final int elapsedTimeSinceLastTyping = (int)(downEventTime - lastLetterTypingTime); + mRecognitionPoints.addDownEventPoint( + x, y, elapsedTimeSinceFirstDown, elapsedTimeSinceLastTyping); + } + + /** + * Add a move event point. + * @param x the x-coordinate of this move event. + * @param y the y-coordinate of this move event. + * @param moveEventTime the time of this move event. + * @param isMajorEvent false if this is a historical move event. + * @param listener {@link BatchInputArbiterListener#onStartUpdateBatchInputTimer()} of this + * <code>listener</code> may be called if enough move points have been added. + * @return true if this move event occurs on the valid gesture area. + */ + public boolean addMoveEventPoint(final int x, final int y, final long moveEventTime, + final boolean isMajorEvent, final BatchInputArbiterListener listener) { + final int beforeLength = mRecognitionPoints.getLength(); + final boolean onValidArea = mRecognitionPoints.addEventPoint( + x, y, getElapsedTimeSinceFirstDown(moveEventTime), isMajorEvent); + if (mRecognitionPoints.getLength() > beforeLength) { + listener.onStartUpdateBatchInputTimer(); + } + return onValidArea; + } + + /** + * Determine whether the batch input has started or not. + * @param listener {@link BatchInputArbiterListener#onStartBatchInput()} of this + * <code>listener</code> will be called when the batch input has started successfully. + * @return true if the batch input has started successfully. + */ + public boolean mayStartBatchInput(final BatchInputArbiterListener listener) { + if (!mRecognitionPoints.isStartOfAGesture()) { + return false; + } + synchronized (sAggregatedPointers) { + sAggregatedPointers.reset(); + sLastRecognitionPointSize = 0; + sLastRecognitionTime = 0; + listener.onStartBatchInput(); + } + return true; + } + + /** + * Add synthetic move event point. After adding the point, + * {@link #updateBatchInput(long,BatchInputArbiterListener)} will be called internally. + * @param syntheticMoveEventTime the synthetic move event time. + * @param listener the listener to be passed to + * {@link #updateBatchInput(long,BatchInputArbiterListener)}. + */ + public void updateBatchInputByTimer(final long syntheticMoveEventTime, + final BatchInputArbiterListener listener) { + mRecognitionPoints.duplicateLastPointWith( + getElapsedTimeSinceFirstDown(syntheticMoveEventTime)); + updateBatchInput(syntheticMoveEventTime, listener); + } + + /** + * Determine whether we have enough gesture points to lookup dictionary. + * @param moveEventTime the time of this move event. + * @param listener {@link BatchInputArbiterListener#onUpdateBatchInput(InputPointers,long)} of + * this <code>listener</code> will be called when enough event points we have. Also + * {@link BatchInputArbiterListener#onStartUpdateBatchInputTimer()} will be called to have + * possible future synthetic move event. + */ + public void updateBatchInput(final long moveEventTime, + final BatchInputArbiterListener listener) { + synchronized (sAggregatedPointers) { + mRecognitionPoints.appendIncrementalBatchPoints(sAggregatedPointers); + final int size = sAggregatedPointers.getPointerSize(); + if (size > sLastRecognitionPointSize && mRecognitionPoints.hasRecognitionTimePast( + moveEventTime, sLastRecognitionTime)) { + listener.onUpdateBatchInput(sAggregatedPointers, moveEventTime); + listener.onStartUpdateBatchInputTimer(); + // The listener may change the size of the pointers (when auto-committing + // for example), so we need to get the size from the pointers again. + sLastRecognitionPointSize = sAggregatedPointers.getPointerSize(); + sLastRecognitionTime = moveEventTime; + } + } + } + + /** + * Determine whether the batch input has ended successfully or continues. + * @param upEventTime the time of this up event. + * @param activePointerCount the number of active pointers when this pointer up event occurs. + * @param listener {@link BatchInputArbiterListener#onEndBatchInput(InputPointers,long)} of this + * <code>listener</code> will be called when the batch input has started successfully. + * @return true if the batch input has ended successfully. + */ + public boolean mayEndBatchInput(final long upEventTime, final int activePointerCount, + final BatchInputArbiterListener listener) { + synchronized (sAggregatedPointers) { + mRecognitionPoints.appendAllBatchPoints(sAggregatedPointers); + if (activePointerCount == 1) { + listener.onEndBatchInput(sAggregatedPointers, upEventTime); + return true; + } + } + return false; + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/BogusMoveEventDetector.java b/java/src/com/android/inputmethod/keyboard/internal/BogusMoveEventDetector.java new file mode 100644 index 000000000..e0589fc97 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/BogusMoveEventDetector.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.internal; + +import android.content.res.Resources; +import android.util.DisplayMetrics; +import android.util.Log; + +import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.LatinImeLogger; +import com.android.inputmethod.latin.R; + +// This hack is applied to certain classes of tablets. +public final class BogusMoveEventDetector { + private static final String TAG = BogusMoveEventDetector.class.getSimpleName(); + private static final boolean DEBUG_MODE = LatinImeLogger.sDBG; + + // Move these thresholds to resource. + // These thresholds' unit is a diagonal length of a key. + private static final float BOGUS_MOVE_ACCUMULATED_DISTANCE_THRESHOLD = 0.53f; + private static final float BOGUS_MOVE_RADIUS_THRESHOLD = 1.14f; + + private static boolean sNeedsProximateBogusDownMoveUpEventHack; + + public static void init(final Resources res) { + // The proximate bogus down move up event hack is needed for a device such like, + // 1) is large tablet, or 2) is small tablet and the screen density is less than hdpi. + // Though it seems odd to use screen density as criteria of the quality of the touch + // screen, the small table that has a less density screen than hdpi most likely has been + // made with the touch screen that needs the hack. + final int screenMetrics = res.getInteger(R.integer.config_screen_metrics); + final boolean isLargeTablet = (screenMetrics == Constants.SCREEN_METRICS_LARGE_TABLET); + final boolean isSmallTablet = (screenMetrics == Constants.SCREEN_METRICS_SMALL_TABLET); + final int densityDpi = res.getDisplayMetrics().densityDpi; + final boolean hasLowDensityScreen = (densityDpi < DisplayMetrics.DENSITY_HIGH); + final boolean needsTheHack = isLargeTablet || (isSmallTablet && hasLowDensityScreen); + if (DEBUG_MODE) { + final int sw = res.getConfiguration().smallestScreenWidthDp; + Log.d(TAG, "needsProximateBogusDownMoveUpEventHack=" + needsTheHack + + " smallestScreenWidthDp=" + sw + " densityDpi=" + densityDpi + + " screenMetrics=" + screenMetrics); + } + sNeedsProximateBogusDownMoveUpEventHack = needsTheHack; + } + + private int mAccumulatedDistanceThreshold; + private int mRadiusThreshold; + + // Accumulated distance from actual and artificial down keys. + /* package */ int mAccumulatedDistanceFromDownKey; + private int mActualDownX; + private int mActualDownY; + + public void setKeyboardGeometry(final int keyWidth, final int keyHeight) { + final float keyDiagonal = (float)Math.hypot(keyWidth, keyHeight); + mAccumulatedDistanceThreshold = (int)( + keyDiagonal * BOGUS_MOVE_ACCUMULATED_DISTANCE_THRESHOLD); + mRadiusThreshold = (int)(keyDiagonal * BOGUS_MOVE_RADIUS_THRESHOLD); + } + + public void onActualDownEvent(final int x, final int y) { + mActualDownX = x; + mActualDownY = y; + } + + public void onDownKey() { + mAccumulatedDistanceFromDownKey = 0; + } + + public void onMoveKey(final int distance) { + mAccumulatedDistanceFromDownKey += distance; + } + + public boolean hasTraveledLongDistance(final int x, final int y) { + if (!sNeedsProximateBogusDownMoveUpEventHack) { + return false; + } + final int dx = Math.abs(x - mActualDownX); + final int dy = Math.abs(y - mActualDownY); + // A bogus move event should be a horizontal movement. A vertical movement might be + // a sloppy typing and should be ignored. + return dx >= dy && mAccumulatedDistanceFromDownKey >= mAccumulatedDistanceThreshold; + } + + public int getAccumulatedDistanceFromDownKey() { + return mAccumulatedDistanceFromDownKey; + } + + public int getDistanceFromDownEvent(final int x, final int y) { + return getDistance(x, y, mActualDownX, mActualDownY); + } + + private static int getDistance(final int x1, final int y1, final int x2, final int y2) { + return (int)Math.hypot(x1 - x2, y1 - y2); + } + + public boolean isCloseToActualDownEvent(final int x, final int y) { + return sNeedsProximateBogusDownMoveUpEventHack + && getDistanceFromDownEvent(x, y) < mRadiusThreshold; + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java b/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java index 4ccecb2f0..dce7fc57e 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java +++ b/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java @@ -17,6 +17,7 @@ package com.android.inputmethod.keyboard.internal; import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.utils.StringUtils; import android.text.TextUtils; @@ -29,15 +30,16 @@ import android.text.TextUtils; * marker. An output text may consist of multiple code points separated by comma. * The format of the codesArray element should be: * <pre> - * codePointInHex[,codePoint2InHex]*(|outputTextCodePointInHex[,outputTextCodePoint2InHex]*)? + * label1[,label2]*(|outputText1[,outputText2]*(|minSupportSdkVersion)?)? * </pre> */ // TODO: Write unit tests for this class. public final class CodesArrayParser { // Constants for parsing. - private static final char COMMA = ','; - private static final String VERTICAL_BAR_STRING = "\\|"; - private static final String COMMA_STRING = ","; + private static final char COMMA = Constants.CODE_COMMA; + private static final String COMMA_REGEX = StringUtils.newSingleCodePointString(COMMA); + private static final String VERTICAL_BAR_REGEX = // "\\|" + new String(new char[] { Constants.CODE_BACKSLASH, Constants.CODE_VERTICAL_BAR }); private static final int BASE_HEX = 16; private CodesArrayParser() { @@ -45,7 +47,7 @@ public final class CodesArrayParser { } private static String getLabelSpec(final String codesArraySpec) { - final String[] strs = codesArraySpec.split(VERTICAL_BAR_STRING, -1); + final String[] strs = codesArraySpec.split(VERTICAL_BAR_REGEX, -1); if (strs.length <= 1) { return codesArraySpec; } @@ -55,7 +57,7 @@ public final class CodesArrayParser { public static String parseLabel(final String codesArraySpec) { final String labelSpec = getLabelSpec(codesArraySpec); final StringBuilder sb = new StringBuilder(); - for (final String codeInHex : labelSpec.split(COMMA_STRING)) { + for (final String codeInHex : labelSpec.split(COMMA_REGEX)) { final int codePoint = Integer.parseInt(codeInHex, BASE_HEX); sb.appendCodePoint(codePoint); } @@ -63,17 +65,15 @@ public final class CodesArrayParser { } private static String getCodeSpec(final String codesArraySpec) { - final String[] strs = codesArraySpec.split(VERTICAL_BAR_STRING, -1); + final String[] strs = codesArraySpec.split(VERTICAL_BAR_REGEX, -1); if (strs.length <= 1) { return codesArraySpec; } return TextUtils.isEmpty(strs[1]) ? strs[0] : strs[1]; } - // codesArraySpec consists of: - // <label>|<code0>,<code1>,...|<minSupportSdkVersion> public static int getMinSupportSdkVersion(final String codesArraySpec) { - final String[] strs = codesArraySpec.split(VERTICAL_BAR_STRING, -1); + final String[] strs = codesArraySpec.split(VERTICAL_BAR_REGEX, -1); if (strs.length <= 2) { return 0; } @@ -98,7 +98,7 @@ public final class CodesArrayParser { return null; } final StringBuilder sb = new StringBuilder(); - for (final String codeInHex : codeSpec.split(COMMA_STRING)) { + for (final String codeInHex : codeSpec.split(COMMA_REGEX)) { final int codePoint = Integer.parseInt(codeInHex, BASE_HEX); sb.appendCodePoint(codePoint); } diff --git a/java/src/com/android/inputmethod/keyboard/internal/DrawingHandler.java b/java/src/com/android/inputmethod/keyboard/internal/DrawingHandler.java new file mode 100644 index 000000000..df82becae --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/DrawingHandler.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.internal; + +import android.os.Message; + +import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.keyboard.internal.DrawingHandler.Callbacks; +import com.android.inputmethod.latin.SuggestedWords; +import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper; + +// TODO: Separate this class into KeyPreviewHandler and BatchInputPreviewHandler or so. +public class DrawingHandler extends LeakGuardHandlerWrapper<Callbacks> { + public interface Callbacks { + public void dismissKeyPreviewWithoutDelay(Key key); + public void dismissAllKeyPreviews(); + public void showGestureFloatingPreviewText(SuggestedWords suggestedWords); + } + + private static final int MSG_DISMISS_KEY_PREVIEW = 0; + private static final int MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1; + + public DrawingHandler(final Callbacks ownerInstance) { + super(ownerInstance); + } + + @Override + public void handleMessage(final Message msg) { + final Callbacks callbacks = getOwnerInstance(); + if (callbacks == null) { + return; + } + switch (msg.what) { + case MSG_DISMISS_KEY_PREVIEW: + callbacks.dismissKeyPreviewWithoutDelay((Key)msg.obj); + break; + case MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT: + callbacks.showGestureFloatingPreviewText(SuggestedWords.EMPTY); + break; + } + } + + public void dismissKeyPreview(final long delay, final Key key) { + sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, key), delay); + } + + private void cancelAllDismissKeyPreviews() { + removeMessages(MSG_DISMISS_KEY_PREVIEW); + final Callbacks callbacks = getOwnerInstance(); + if (callbacks == null) { + return; + } + callbacks.dismissAllKeyPreviews(); + } + + public void dismissGestureFloatingPreviewText(final long delay) { + sendMessageDelayed(obtainMessage(MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT), delay); + } + + public void cancelAllMessages() { + cancelAllDismissKeyPreviews(); + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java b/java/src/com/android/inputmethod/keyboard/internal/DrawingPreviewPlacerView.java index 4c8607da8..a5d47adb3 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java +++ b/java/src/com/android/inputmethod/keyboard/internal/DrawingPreviewPlacerView.java @@ -24,17 +24,16 @@ import android.graphics.PorterDuffXfermode; import android.util.AttributeSet; import android.widget.RelativeLayout; -import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.CoordinateUtils; import java.util.ArrayList; -public final class PreviewPlacerView extends RelativeLayout { +public final class DrawingPreviewPlacerView extends RelativeLayout { private final int[] mKeyboardViewOrigin = CoordinateUtils.newInstance(); - private final ArrayList<AbstractDrawingPreview> mPreviews = CollectionUtils.newArrayList(); + private final ArrayList<AbstractDrawingPreview> mPreviews = new ArrayList<>(); - public PreviewPlacerView(final Context context, final AttributeSet attrs) { + public DrawingPreviewPlacerView(final Context context, final AttributeSet attrs) { super(context, attrs); setWillNotDraw(false); } @@ -47,7 +46,9 @@ public final class PreviewPlacerView extends RelativeLayout { } public void addPreview(final AbstractDrawingPreview preview) { - mPreviews.add(preview); + if (mPreviews.indexOf(preview) < 0) { + mPreviews.add(preview); + } } public void setKeyboardViewGeometry(final int[] originCoords, final int width, @@ -55,20 +56,24 @@ public final class PreviewPlacerView extends RelativeLayout { CoordinateUtils.copy(mKeyboardViewOrigin, originCoords); final int count = mPreviews.size(); for (int i = 0; i < count; i++) { - mPreviews.get(i).setKeyboardGeometry(originCoords, width, height); + mPreviews.get(i).setKeyboardViewGeometry(originCoords, width, height); } } - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); + public void deallocateMemory() { final int count = mPreviews.size(); for (int i = 0; i < count; i++) { - mPreviews.get(i).onDetachFromWindow(); + mPreviews.get(i).onDeallocateMemory(); } } @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + deallocateMemory(); + } + + @Override public void onDraw(final Canvas canvas) { super.onDraw(canvas); final int originX = CoordinateUtils.x(mKeyboardViewOrigin); diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureEnabler.java b/java/src/com/android/inputmethod/keyboard/internal/GestureEnabler.java new file mode 100644 index 000000000..7d14ae924 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/GestureEnabler.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.internal; + +import com.android.inputmethod.accessibility.AccessibilityUtils; + +public final class GestureEnabler { + /** True if we should handle gesture events. */ + private boolean mShouldHandleGesture; + private boolean mMainDictionaryAvailable; + private boolean mGestureHandlingEnabledByInputField; + private boolean mGestureHandlingEnabledByUser; + + private void updateGestureHandlingMode() { + mShouldHandleGesture = mMainDictionaryAvailable + && mGestureHandlingEnabledByInputField + && mGestureHandlingEnabledByUser + && !AccessibilityUtils.getInstance().isTouchExplorationEnabled(); + } + + // Note that this method is called from a non-UI thread. + public void setMainDictionaryAvailability(final boolean mainDictionaryAvailable) { + mMainDictionaryAvailable = mainDictionaryAvailable; + updateGestureHandlingMode(); + } + + public void setGestureHandlingEnabledByUser(final boolean gestureHandlingEnabledByUser) { + mGestureHandlingEnabledByUser = gestureHandlingEnabledByUser; + updateGestureHandlingMode(); + } + + public void setPasswordMode(final boolean passwordMode) { + mGestureHandlingEnabledByInputField = !passwordMode; + updateGestureHandlingMode(); + } + + public boolean shouldHandleGesture() { + return mShouldHandleGesture; + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingPreviewText.java b/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingTextDrawingPreview.java index c6dd9e100..fd84856b7 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingPreviewText.java +++ b/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingTextDrawingPreview.java @@ -23,7 +23,6 @@ import android.graphics.Paint.Align; import android.graphics.Rect; import android.graphics.RectF; import android.text.TextUtils; -import android.view.View; import com.android.inputmethod.keyboard.PointerTracker; import com.android.inputmethod.latin.R; @@ -42,13 +41,14 @@ import com.android.inputmethod.latin.utils.CoordinateUtils; * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewVerticalPadding * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewRoundRadius */ -public class GestureFloatingPreviewText extends AbstractDrawingPreview { +public class GestureFloatingTextDrawingPreview extends AbstractDrawingPreview { protected static final class GesturePreviewTextParams { public final int mGesturePreviewTextOffset; public final int mGesturePreviewTextHeight; public final float mGesturePreviewHorizontalPadding; public final float mGesturePreviewVerticalPadding; public final float mGesturePreviewRoundRadius; + public final int mDisplayWidth; private final int mGesturePreviewTextSize; private final int mGesturePreviewTextColor; @@ -72,6 +72,7 @@ public class GestureFloatingPreviewText extends AbstractDrawingPreview { R.styleable.MainKeyboardView_gestureFloatingPreviewVerticalPadding, 0.0f); mGesturePreviewRoundRadius = mainKeyboardViewAttr.getDimension( R.styleable.MainKeyboardView_gestureFloatingPreviewRoundRadius, 0.0f); + mDisplayWidth = mainKeyboardViewAttr.getResources().getDisplayMetrics().widthPixels; final Paint textPaint = getTextPaint(); final Rect textRect = new Rect(); @@ -100,9 +101,13 @@ public class GestureFloatingPreviewText extends AbstractDrawingPreview { private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY; private final int[] mLastPointerCoords = CoordinateUtils.newInstance(); - public GestureFloatingPreviewText(final View drawingView, final TypedArray typedArray) { - super(drawingView); - mParams = new GesturePreviewTextParams(typedArray); + public GestureFloatingTextDrawingPreview(final TypedArray mainKeyboardViewAttr) { + mParams = new GesturePreviewTextParams(mainKeyboardViewAttr); + } + + @Override + public void onDeallocateMemory() { + // Nothing to do here. } public void setSuggetedWords(final SuggestedWords suggestedWords) { @@ -144,7 +149,7 @@ public class GestureFloatingPreviewText extends AbstractDrawingPreview { */ protected void updatePreviewPosition() { if (mSuggestedWords.isEmpty() || TextUtils.isEmpty(mSuggestedWords.getWord(0))) { - getDrawingView().invalidate(); + invalidateDrawingView(); return; } final String text = mSuggestedWords.getWord(0); @@ -158,10 +163,9 @@ public class GestureFloatingPreviewText extends AbstractDrawingPreview { final float rectWidth = textWidth + hPad * 2.0f; final float rectHeight = textHeight + vPad * 2.0f; - final int displayWidth = getDrawingView().getResources().getDisplayMetrics().widthPixels; final float rectX = Math.min( Math.max(CoordinateUtils.x(mLastPointerCoords) - rectWidth / 2.0f, 0.0f), - displayWidth - rectWidth); + mParams.mDisplayWidth - rectWidth); final float rectY = CoordinateUtils.y(mLastPointerCoords) - mParams.mGesturePreviewTextOffset - rectHeight; rectangle.set(rectX, rectY, rectX + rectWidth, rectY + rectHeight); @@ -169,6 +173,6 @@ public class GestureFloatingPreviewText extends AbstractDrawingPreview { mPreviewTextX = (int)(rectX + hPad + textWidth / 2.0f); mPreviewTextY = (int)(rectY + vPad) + textHeight; // TODO: Should narrow the invalidate region. - getDrawingView().invalidate(); + invalidateDrawingView(); } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeDrawingParams.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeDrawingParams.java new file mode 100644 index 000000000..478639d2d --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeDrawingParams.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.internal; + +import android.content.res.TypedArray; + +import com.android.inputmethod.latin.R; + +/** + * This class holds parameters to control how a gesture stroke is sampled and drawn on the screen. + * + * @attr ref R.styleable#MainKeyboardView_gestureTrailMinSamplingDistance + * @attr ref R.styleable#MainKeyboardView_gestureTrailMaxInterpolationAngularThreshold + * @attr ref R.styleable#MainKeyboardView_gestureTrailMaxInterpolationDistanceThreshold + * @attr ref R.styleable#MainKeyboardView_gestureTrailMaxInterpolationSegments + */ +public final class GestureStrokeDrawingParams { + public final double mMinSamplingDistance; // in pixel + public final double mMaxInterpolationAngularThreshold; // in radian + public final double mMaxInterpolationDistanceThreshold; // in pixel + public final int mMaxInterpolationSegments; + + private static final float DEFAULT_MIN_SAMPLING_DISTANCE = 0.0f; // dp + private static final int DEFAULT_MAX_INTERPOLATION_ANGULAR_THRESHOLD = 15; // in degree + private static final float DEFAULT_MAX_INTERPOLATION_DISTANCE_THRESHOLD = 0.0f; // dp + private static final int DEFAULT_MAX_INTERPOLATION_SEGMENTS = 4; + + public GestureStrokeDrawingParams(final TypedArray mainKeyboardViewAttr) { + mMinSamplingDistance = mainKeyboardViewAttr.getDimension( + R.styleable.MainKeyboardView_gestureTrailMinSamplingDistance, + DEFAULT_MIN_SAMPLING_DISTANCE); + final int interpolationAngularDegree = mainKeyboardViewAttr.getInteger(R.styleable + .MainKeyboardView_gestureTrailMaxInterpolationAngularThreshold, 0); + mMaxInterpolationAngularThreshold = (interpolationAngularDegree <= 0) + ? Math.toRadians(DEFAULT_MAX_INTERPOLATION_ANGULAR_THRESHOLD) + : Math.toRadians(interpolationAngularDegree); + mMaxInterpolationDistanceThreshold = mainKeyboardViewAttr.getDimension(R.styleable + .MainKeyboardView_gestureTrailMaxInterpolationDistanceThreshold, + DEFAULT_MAX_INTERPOLATION_DISTANCE_THRESHOLD); + mMaxInterpolationSegments = mainKeyboardViewAttr.getInteger( + R.styleable.MainKeyboardView_gestureTrailMaxInterpolationSegments, + DEFAULT_MAX_INTERPOLATION_SEGMENTS); + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeDrawingPoints.java index ecc67dd44..7d09e9d2f 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java +++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeDrawingPoints.java @@ -16,19 +16,19 @@ package com.android.inputmethod.keyboard.internal; -import android.content.res.TypedArray; - -import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.utils.ResizableIntArray; -public final class GestureStrokeWithPreviewPoints extends GestureStroke { +/** + * This class holds drawing points to represent a gesture stroke on the screen. + */ +public final class GestureStrokeDrawingPoints { public static final int PREVIEW_CAPACITY = 256; private final ResizableIntArray mPreviewEventTimes = new ResizableIntArray(PREVIEW_CAPACITY); private final ResizableIntArray mPreviewXCoordinates = new ResizableIntArray(PREVIEW_CAPACITY); private final ResizableIntArray mPreviewYCoordinates = new ResizableIntArray(PREVIEW_CAPACITY); - private final GestureStrokePreviewParams mPreviewParams; + private final GestureStrokeDrawingParams mDrawingParams; private int mStrokeId; private int mLastPreviewSize; @@ -39,56 +39,11 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke { private int mLastY; private double mDistanceFromLastSample; - public static final class GestureStrokePreviewParams { - public final double mMinSamplingDistance; // in pixel - public final double mMaxInterpolationAngularThreshold; // in radian - public final double mMaxInterpolationDistanceThreshold; // in pixel - public final int mMaxInterpolationSegments; - - public static final GestureStrokePreviewParams DEFAULT = new GestureStrokePreviewParams(); - - private static final int DEFAULT_MAX_INTERPOLATION_ANGULAR_THRESHOLD = 15; // in degree - - private GestureStrokePreviewParams() { - mMinSamplingDistance = 0.0d; - mMaxInterpolationAngularThreshold = - degreeToRadian(DEFAULT_MAX_INTERPOLATION_ANGULAR_THRESHOLD); - mMaxInterpolationDistanceThreshold = mMinSamplingDistance; - mMaxInterpolationSegments = 4; - } - - private static double degreeToRadian(final int degree) { - return degree / 180.0d * Math.PI; - } - - public GestureStrokePreviewParams(final TypedArray mainKeyboardViewAttr) { - mMinSamplingDistance = mainKeyboardViewAttr.getDimension( - R.styleable.MainKeyboardView_gestureTrailMinSamplingDistance, - (float)DEFAULT.mMinSamplingDistance); - final int interpolationAngularDegree = mainKeyboardViewAttr.getInteger(R.styleable - .MainKeyboardView_gestureTrailMaxInterpolationAngularThreshold, 0); - mMaxInterpolationAngularThreshold = (interpolationAngularDegree <= 0) - ? DEFAULT.mMaxInterpolationAngularThreshold - : degreeToRadian(interpolationAngularDegree); - mMaxInterpolationDistanceThreshold = mainKeyboardViewAttr.getDimension(R.styleable - .MainKeyboardView_gestureTrailMaxInterpolationDistanceThreshold, - (float)DEFAULT.mMaxInterpolationDistanceThreshold); - mMaxInterpolationSegments = mainKeyboardViewAttr.getInteger( - R.styleable.MainKeyboardView_gestureTrailMaxInterpolationSegments, - DEFAULT.mMaxInterpolationSegments); - } - } - - public GestureStrokeWithPreviewPoints(final int pointerId, - final GestureStrokeParams strokeParams, - final GestureStrokePreviewParams previewParams) { - super(pointerId, strokeParams); - mPreviewParams = previewParams; + public GestureStrokeDrawingPoints(final GestureStrokeDrawingParams drawingParams) { + mDrawingParams = drawingParams; } - @Override - protected void reset() { - super.reset(); + private void reset() { mStrokeId++; mLastPreviewSize = 0; mLastInterpolatedPreviewIndex = 0; @@ -101,28 +56,29 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke { return mStrokeId; } + public void onDownEvent(final int x, final int y, final int elapsedTimeSinceFirstDown) { + reset(); + onMoveEvent(x, y, elapsedTimeSinceFirstDown); + } + private boolean needsSampling(final int x, final int y) { mDistanceFromLastSample += Math.hypot(x - mLastX, y - mLastY); mLastX = x; mLastY = y; final boolean isDownEvent = (mPreviewEventTimes.getLength() == 0); - if (mDistanceFromLastSample >= mPreviewParams.mMinSamplingDistance || isDownEvent) { + if (mDistanceFromLastSample >= mDrawingParams.mMinSamplingDistance || isDownEvent) { mDistanceFromLastSample = 0.0d; return true; } return false; } - @Override - public boolean addPointOnKeyboard(final int x, final int y, final int time, - final boolean isMajorEvent) { + public void onMoveEvent(final int x, final int y, final int elapsedTimeSinceFirstDown) { if (needsSampling(x, y)) { - mPreviewEventTimes.add(time); + mPreviewEventTimes.add(elapsedTimeSinceFirstDown); mPreviewXCoordinates.add(x); mPreviewYCoordinates.add(y); } - return super.addPointOnKeyboard(x, y, time, isMajorEvent); - } /** @@ -132,7 +88,7 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke { * @param xCoords the x-coordinates array of gesture trail to be drawn. * @param yCoords the y-coordinates array of gesture trail to be drawn. * @param types the point types array of gesture trail. This is valid only when - * {@link GestureTrail#DEBUG_SHOW_POINTS} is true. + * {@link GestureTrailDrawingPoints#DEBUG_SHOW_POINTS} is true. */ public void appendPreviewStroke(final ResizableIntArray eventTimes, final ResizableIntArray xCoords, final ResizableIntArray yCoords, @@ -144,8 +100,8 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke { eventTimes.append(mPreviewEventTimes, mLastPreviewSize, length); xCoords.append(mPreviewXCoordinates, mLastPreviewSize, length); yCoords.append(mPreviewYCoordinates, mLastPreviewSize, length); - if (GestureTrail.DEBUG_SHOW_POINTS) { - types.fill(GestureTrail.POINT_TYPE_SAMPLED, types.getLength(), length); + if (GestureTrailDrawingPoints.DEBUG_SHOW_POINTS) { + types.fill(GestureTrailDrawingPoints.POINT_TYPE_SAMPLED, types.getLength(), length); } mLastPreviewSize = mPreviewEventTimes.getLength(); } @@ -162,7 +118,7 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke { * @param xCoords the x-coordinates array of gesture trail to be drawn. * @param yCoords the y-coordinates array of gesture trail to be drawn. * @param types the point types array of gesture trail. This is valid only when - * {@link GestureTrail#DEBUG_SHOW_POINTS} is true. + * {@link GestureTrailDrawingPoints#DEBUG_SHOW_POINTS} is true. * @return the start index of the last interpolated segment of input arrays. */ public int interpolateStrokeAndReturnStartIndexOfLastSegment(final int lastInterpolatedIndex, @@ -188,12 +144,12 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke { final double m2 = Math.atan2(mInterpolator.mSlope2Y, mInterpolator.mSlope2X); final double deltaAngle = Math.abs(angularDiff(m2, m1)); final int segmentsByAngle = (int)Math.ceil( - deltaAngle / mPreviewParams.mMaxInterpolationAngularThreshold); + deltaAngle / mDrawingParams.mMaxInterpolationAngularThreshold); final double deltaDistance = Math.hypot(mInterpolator.mP1X - mInterpolator.mP2X, mInterpolator.mP1Y - mInterpolator.mP2Y); final int segmentsByDistance = (int)Math.ceil(deltaDistance - / mPreviewParams.mMaxInterpolationDistanceThreshold); - final int segments = Math.min(mPreviewParams.mMaxInterpolationSegments, + / mDrawingParams.mMaxInterpolationDistanceThreshold); + final int segments = Math.min(mDrawingParams.mMaxInterpolationSegments, Math.max(segmentsByAngle, segmentsByDistance)); final int t1 = eventTimes.get(d1); final int dt = pt[p2] - pt[p1]; @@ -201,19 +157,19 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke { for (int i = 1; i < segments; i++) { final float t = i / (float)segments; mInterpolator.interpolate(t); - eventTimes.add(d1, (int)(dt * t) + t1); - xCoords.add(d1, (int)mInterpolator.mInterpolatedX); - yCoords.add(d1, (int)mInterpolator.mInterpolatedY); - if (GestureTrail.DEBUG_SHOW_POINTS) { - types.add(d1, GestureTrail.POINT_TYPE_INTERPOLATED); + eventTimes.addAt(d1, (int)(dt * t) + t1); + xCoords.addAt(d1, (int)mInterpolator.mInterpolatedX); + yCoords.addAt(d1, (int)mInterpolator.mInterpolatedY); + if (GestureTrailDrawingPoints.DEBUG_SHOW_POINTS) { + types.addAt(d1, GestureTrailDrawingPoints.POINT_TYPE_INTERPOLATED); } d1++; } - eventTimes.add(d1, pt[p2]); - xCoords.add(d1, px[p2]); - yCoords.add(d1, py[p2]); - if (GestureTrail.DEBUG_SHOW_POINTS) { - types.add(d1, GestureTrail.POINT_TYPE_SAMPLED); + eventTimes.addAt(d1, pt[p2]); + xCoords.addAt(d1, px[p2]); + yCoords.addAt(d1, py[p2]); + if (GestureTrailDrawingPoints.DEBUG_SHOW_POINTS) { + types.addAt(d1, GestureTrailDrawingPoints.POINT_TYPE_SAMPLED); } } return lastInterpolatedDrawIndex; diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeRecognitionParams.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeRecognitionParams.java new file mode 100644 index 000000000..07b14514c --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeRecognitionParams.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.internal; + +import android.content.res.TypedArray; + +import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.utils.ResourceUtils; + +/** + * This class holds parameters to control how a gesture stroke is sampled and recognized. + * This class also has parameters to distinguish gesture input events from fast typing events. + * + * @attr ref R.styleable#MainKeyboardView_gestureStaticTimeThresholdAfterFastTyping + * @attr ref R.styleable#MainKeyboardView_gestureDetectFastMoveSpeedThreshold + * @attr ref R.styleable#MainKeyboardView_gestureDynamicThresholdDecayDuration + * @attr ref R.styleable#MainKeyboardView_gestureDynamicTimeThresholdFrom + * @attr ref R.styleable#MainKeyboardView_gestureDynamicTimeThresholdTo + * @attr ref R.styleable#MainKeyboardView_gestureDynamicDistanceThresholdFrom + * @attr ref R.styleable#MainKeyboardView_gestureDynamicDistanceThresholdTo + * @attr ref R.styleable#MainKeyboardView_gestureSamplingMinimumDistance + * @attr ref R.styleable#MainKeyboardView_gestureRecognitionMinimumTime + * @attr ref R.styleable#MainKeyboardView_gestureRecognitionSpeedThreshold + */ +public final class GestureStrokeRecognitionParams { + // Static threshold for gesture after fast typing + public final int mStaticTimeThresholdAfterFastTyping; // msec + // Static threshold for starting gesture detection + public final float mDetectFastMoveSpeedThreshold; // keyWidth/sec + // Dynamic threshold for gesture after fast typing + public final int mDynamicThresholdDecayDuration; // msec + // Time based threshold values + public final int mDynamicTimeThresholdFrom; // msec + public final int mDynamicTimeThresholdTo; // msec + // Distance based threshold values + public final float mDynamicDistanceThresholdFrom; // keyWidth + public final float mDynamicDistanceThresholdTo; // keyWidth + // Parameters for gesture sampling + public final float mSamplingMinimumDistance; // keyWidth + // Parameters for gesture recognition + public final int mRecognitionMinimumTime; // msec + public final float mRecognitionSpeedThreshold; // keyWidth/sec + + // Default GestureStrokeRecognitionPoints parameters. + public static final GestureStrokeRecognitionParams DEFAULT = + new GestureStrokeRecognitionParams(); + + private GestureStrokeRecognitionParams() { + // These parameter values are default and intended for testing. + mStaticTimeThresholdAfterFastTyping = 350; // msec + mDetectFastMoveSpeedThreshold = 1.5f; // keyWidth/sec + mDynamicThresholdDecayDuration = 450; // msec + mDynamicTimeThresholdFrom = 300; // msec + mDynamicTimeThresholdTo = 20; // msec + mDynamicDistanceThresholdFrom = 6.0f; // keyWidth + mDynamicDistanceThresholdTo = 0.35f; // keyWidth + // The following parameters' change will affect the result of regression test. + mSamplingMinimumDistance = 1.0f / 6.0f; // keyWidth + mRecognitionMinimumTime = 100; // msec + mRecognitionSpeedThreshold = 5.5f; // keyWidth/sec + } + + public GestureStrokeRecognitionParams(final TypedArray mainKeyboardViewAttr) { + mStaticTimeThresholdAfterFastTyping = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_gestureStaticTimeThresholdAfterFastTyping, + DEFAULT.mStaticTimeThresholdAfterFastTyping); + mDetectFastMoveSpeedThreshold = ResourceUtils.getFraction(mainKeyboardViewAttr, + R.styleable.MainKeyboardView_gestureDetectFastMoveSpeedThreshold, + DEFAULT.mDetectFastMoveSpeedThreshold); + mDynamicThresholdDecayDuration = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_gestureDynamicThresholdDecayDuration, + DEFAULT.mDynamicThresholdDecayDuration); + mDynamicTimeThresholdFrom = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_gestureDynamicTimeThresholdFrom, + DEFAULT.mDynamicTimeThresholdFrom); + mDynamicTimeThresholdTo = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_gestureDynamicTimeThresholdTo, + DEFAULT.mDynamicTimeThresholdTo); + mDynamicDistanceThresholdFrom = ResourceUtils.getFraction(mainKeyboardViewAttr, + R.styleable.MainKeyboardView_gestureDynamicDistanceThresholdFrom, + DEFAULT.mDynamicDistanceThresholdFrom); + mDynamicDistanceThresholdTo = ResourceUtils.getFraction(mainKeyboardViewAttr, + R.styleable.MainKeyboardView_gestureDynamicDistanceThresholdTo, + DEFAULT.mDynamicDistanceThresholdTo); + mSamplingMinimumDistance = ResourceUtils.getFraction(mainKeyboardViewAttr, + R.styleable.MainKeyboardView_gestureSamplingMinimumDistance, + DEFAULT.mSamplingMinimumDistance); + mRecognitionMinimumTime = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_gestureRecognitionMinimumTime, + DEFAULT.mRecognitionMinimumTime); + mRecognitionSpeedThreshold = ResourceUtils.getFraction(mainKeyboardViewAttr, + R.styleable.MainKeyboardView_gestureRecognitionSpeedThreshold, + DEFAULT.mRecognitionSpeedThreshold); + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeRecognitionPoints.java index f29ade861..e49e538aa 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java +++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeRecognitionPoints.java @@ -16,16 +16,18 @@ package com.android.inputmethod.keyboard.internal; -import android.content.res.TypedArray; import android.util.Log; +import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.InputPointers; -import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.utils.ResizableIntArray; -import com.android.inputmethod.latin.utils.ResourceUtils; -public class GestureStroke { - private static final String TAG = GestureStroke.class.getSimpleName(); +/** + * This class holds event points to recognize a gesture stroke. + * TODO: Should be package private class. + */ +public final class GestureStrokeRecognitionPoints { + private static final String TAG = GestureStrokeRecognitionPoints.class.getSimpleName(); private static final boolean DEBUG = false; private static final boolean DEBUG_SPEED = false; @@ -33,14 +35,15 @@ public class GestureStroke { // Proportional to the keyboard height. public static final float EXTRA_GESTURE_TRAIL_AREA_ABOVE_KEYBOARD_RATIO = 0.25f; - public static final int DEFAULT_CAPACITY = 128; - private final int mPointerId; - private final ResizableIntArray mEventTimes = new ResizableIntArray(DEFAULT_CAPACITY); - private final ResizableIntArray mXCoordinates = new ResizableIntArray(DEFAULT_CAPACITY); - private final ResizableIntArray mYCoordinates = new ResizableIntArray(DEFAULT_CAPACITY); + private final ResizableIntArray mEventTimes = new ResizableIntArray( + Constants.DEFAULT_GESTURE_POINTS_CAPACITY); + private final ResizableIntArray mXCoordinates = new ResizableIntArray( + Constants.DEFAULT_GESTURE_POINTS_CAPACITY); + private final ResizableIntArray mYCoordinates = new ResizableIntArray( + Constants.DEFAULT_GESTURE_POINTS_CAPACITY); - private final GestureStrokeParams mParams; + private final GestureStrokeRecognitionParams mRecognitionParams; private int mKeyWidth; // pixel private int mMinYCoordinate; // pixel @@ -64,145 +67,85 @@ public class GestureStroke { private int mIncrementalRecognitionSize; private int mLastIncrementalBatchSize; - public static final class GestureStrokeParams { - // Static threshold for gesture after fast typing - public final int mStaticTimeThresholdAfterFastTyping; // msec - // Static threshold for starting gesture detection - public final float mDetectFastMoveSpeedThreshold; // keyWidth/sec - // Dynamic threshold for gesture after fast typing - public final int mDynamicThresholdDecayDuration; // msec - // Time based threshold values - public final int mDynamicTimeThresholdFrom; // msec - public final int mDynamicTimeThresholdTo; // msec - // Distance based threshold values - public final float mDynamicDistanceThresholdFrom; // keyWidth - public final float mDynamicDistanceThresholdTo; // keyWidth - // Parameters for gesture sampling - public final float mSamplingMinimumDistance; // keyWidth - // Parameters for gesture recognition - public final int mRecognitionMinimumTime; // msec - public final float mRecognitionSpeedThreshold; // keyWidth/sec - - // Default GestureStroke parameters. - public static final GestureStrokeParams DEFAULT = new GestureStrokeParams(); - - private GestureStrokeParams() { - // These parameter values are default and intended for testing. - mStaticTimeThresholdAfterFastTyping = 350; // msec - mDetectFastMoveSpeedThreshold = 1.5f; // keyWidth / sec - mDynamicThresholdDecayDuration = 450; // msec - mDynamicTimeThresholdFrom = 300; // msec - mDynamicTimeThresholdTo = 20; // msec - mDynamicDistanceThresholdFrom = 6.0f; // keyWidth - mDynamicDistanceThresholdTo = 0.35f; // keyWidth - // The following parameters' change will affect the result of regression test. - mSamplingMinimumDistance = 1.0f / 6.0f; // keyWidth - mRecognitionMinimumTime = 100; // msec - mRecognitionSpeedThreshold = 5.5f; // keyWidth / sec - } - - public GestureStrokeParams(final TypedArray mainKeyboardViewAttr) { - mStaticTimeThresholdAfterFastTyping = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_gestureStaticTimeThresholdAfterFastTyping, - DEFAULT.mStaticTimeThresholdAfterFastTyping); - mDetectFastMoveSpeedThreshold = ResourceUtils.getFraction(mainKeyboardViewAttr, - R.styleable.MainKeyboardView_gestureDetectFastMoveSpeedThreshold, - DEFAULT.mDetectFastMoveSpeedThreshold); - mDynamicThresholdDecayDuration = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_gestureDynamicThresholdDecayDuration, - DEFAULT.mDynamicThresholdDecayDuration); - mDynamicTimeThresholdFrom = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_gestureDynamicTimeThresholdFrom, - DEFAULT.mDynamicTimeThresholdFrom); - mDynamicTimeThresholdTo = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_gestureDynamicTimeThresholdTo, - DEFAULT.mDynamicTimeThresholdTo); - mDynamicDistanceThresholdFrom = ResourceUtils.getFraction(mainKeyboardViewAttr, - R.styleable.MainKeyboardView_gestureDynamicDistanceThresholdFrom, - DEFAULT.mDynamicDistanceThresholdFrom); - mDynamicDistanceThresholdTo = ResourceUtils.getFraction(mainKeyboardViewAttr, - R.styleable.MainKeyboardView_gestureDynamicDistanceThresholdTo, - DEFAULT.mDynamicDistanceThresholdTo); - mSamplingMinimumDistance = ResourceUtils.getFraction(mainKeyboardViewAttr, - R.styleable.MainKeyboardView_gestureSamplingMinimumDistance, - DEFAULT.mSamplingMinimumDistance); - mRecognitionMinimumTime = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_gestureRecognitionMinimumTime, - DEFAULT.mRecognitionMinimumTime); - mRecognitionSpeedThreshold = ResourceUtils.getFraction(mainKeyboardViewAttr, - R.styleable.MainKeyboardView_gestureRecognitionSpeedThreshold, - DEFAULT.mRecognitionSpeedThreshold); - } - } - private static final int MSEC_PER_SEC = 1000; - public GestureStroke(final int pointerId, final GestureStrokeParams params) { + // TODO: Make this package private + public GestureStrokeRecognitionPoints(final int pointerId, + final GestureStrokeRecognitionParams recognitionParams) { mPointerId = pointerId; - mParams = params; + mRecognitionParams = recognitionParams; } + // TODO: Make this package private public void setKeyboardGeometry(final int keyWidth, final int keyboardHeight) { mKeyWidth = keyWidth; mMinYCoordinate = -(int)(keyboardHeight * EXTRA_GESTURE_TRAIL_AREA_ABOVE_KEYBOARD_RATIO); mMaxYCoordinate = keyboardHeight; // TODO: Find an appropriate base metric for these length. Maybe diagonal length of the key? - mDetectFastMoveSpeedThreshold = (int)(keyWidth * mParams.mDetectFastMoveSpeedThreshold); - mGestureDynamicDistanceThresholdFrom = - (int)(keyWidth * mParams.mDynamicDistanceThresholdFrom); - mGestureDynamicDistanceThresholdTo = (int)(keyWidth * mParams.mDynamicDistanceThresholdTo); - mGestureSamplingMinimumDistance = (int)(keyWidth * mParams.mSamplingMinimumDistance); - mGestureRecognitionSpeedThreshold = (int)(keyWidth * mParams.mRecognitionSpeedThreshold); + mDetectFastMoveSpeedThreshold = (int)( + keyWidth * mRecognitionParams.mDetectFastMoveSpeedThreshold); + mGestureDynamicDistanceThresholdFrom = (int)( + keyWidth * mRecognitionParams.mDynamicDistanceThresholdFrom); + mGestureDynamicDistanceThresholdTo = (int)( + keyWidth * mRecognitionParams.mDynamicDistanceThresholdTo); + mGestureSamplingMinimumDistance = (int)( + keyWidth * mRecognitionParams.mSamplingMinimumDistance); + mGestureRecognitionSpeedThreshold = (int)( + keyWidth * mRecognitionParams.mRecognitionSpeedThreshold); if (DEBUG) { Log.d(TAG, String.format( "[%d] setKeyboardGeometry: keyWidth=%3d tT=%3d >> %3d tD=%3d >> %3d", mPointerId, keyWidth, - mParams.mDynamicTimeThresholdFrom, - mParams.mDynamicTimeThresholdTo, + mRecognitionParams.mDynamicTimeThresholdFrom, + mRecognitionParams.mDynamicTimeThresholdTo, mGestureDynamicDistanceThresholdFrom, mGestureDynamicDistanceThresholdTo)); } } + // TODO: Make this package private public int getLength() { return mEventTimes.getLength(); } - public void onDownEvent(final int x, final int y, final long downTime, - final long gestureFirstDownTime, final long lastTypingTime) { + // TODO: Make this package private + public void addDownEventPoint(final int x, final int y, final int elapsedTimeSinceFirstDown, + final int elapsedTimeSinceLastTyping) { reset(); - final long elapsedTimeAfterTyping = downTime - lastTypingTime; - if (elapsedTimeAfterTyping < mParams.mStaticTimeThresholdAfterFastTyping) { + if (elapsedTimeSinceLastTyping < mRecognitionParams.mStaticTimeThresholdAfterFastTyping) { mAfterFastTyping = true; } if (DEBUG) { Log.d(TAG, String.format("[%d] onDownEvent: dT=%3d%s", mPointerId, - elapsedTimeAfterTyping, mAfterFastTyping ? " afterFastTyping" : "")); + elapsedTimeSinceLastTyping, mAfterFastTyping ? " afterFastTyping" : "")); } - final int elapsedTimeFromFirstDown = (int)(downTime - gestureFirstDownTime); - addPointOnKeyboard(x, y, elapsedTimeFromFirstDown, true /* isMajorEvent */); + // Call {@link #addEventPoint(int,int,int,boolean)} to record this down event point as a + // major event point. + addEventPoint(x, y, elapsedTimeSinceFirstDown, true /* isMajorEvent */); } private int getGestureDynamicDistanceThreshold(final int deltaTime) { - if (!mAfterFastTyping || deltaTime >= mParams.mDynamicThresholdDecayDuration) { + if (!mAfterFastTyping || deltaTime >= mRecognitionParams.mDynamicThresholdDecayDuration) { return mGestureDynamicDistanceThresholdTo; } final int decayedThreshold = (mGestureDynamicDistanceThresholdFrom - mGestureDynamicDistanceThresholdTo) - * deltaTime / mParams.mDynamicThresholdDecayDuration; + * deltaTime / mRecognitionParams.mDynamicThresholdDecayDuration; return mGestureDynamicDistanceThresholdFrom - decayedThreshold; } private int getGestureDynamicTimeThreshold(final int deltaTime) { - if (!mAfterFastTyping || deltaTime >= mParams.mDynamicThresholdDecayDuration) { - return mParams.mDynamicTimeThresholdTo; + if (!mAfterFastTyping || deltaTime >= mRecognitionParams.mDynamicThresholdDecayDuration) { + return mRecognitionParams.mDynamicTimeThresholdTo; } final int decayedThreshold = - (mParams.mDynamicTimeThresholdFrom - mParams.mDynamicTimeThresholdTo) - * deltaTime / mParams.mDynamicThresholdDecayDuration; - return mParams.mDynamicTimeThresholdFrom - decayedThreshold; + (mRecognitionParams.mDynamicTimeThresholdFrom + - mRecognitionParams.mDynamicTimeThresholdTo) + * deltaTime / mRecognitionParams.mDynamicThresholdDecayDuration; + return mRecognitionParams.mDynamicTimeThresholdFrom - decayedThreshold; } + // TODO: Make this package private public final boolean isStartOfAGesture() { if (!hasDetectedFastMove()) { return false; @@ -233,6 +176,7 @@ public class GestureStroke { return isStartOfAGesture; } + // TODO: Make this package private public void duplicateLastPointWith(final int time) { final int lastIndex = getLength() - 1; if (lastIndex >= 0) { @@ -248,7 +192,7 @@ public class GestureStroke { } } - protected void reset() { + private void reset() { mIncrementalRecognitionSize = 0; mLastIncrementalBatchSize = 0; mEventTimes.setLength(0); @@ -316,19 +260,20 @@ public class GestureStroke { } /** - * Add a touch event as a gesture point. Returns true if the touch event is on the valid - * gesture area. - * @param x the x-coordinate of the touch event - * @param y the y-coordinate of the touch event + * Add an event point to this gesture stroke recognition points. Returns true if the event + * point is on the valid gesture area. + * @param x the x-coordinate of the event point + * @param y the y-coordinate of the event point * @param time the elapsed time in millisecond from the first gesture down * @param isMajorEvent false if this is a historical move event - * @return true if the touch event is on the valid gesture area + * @return true if the event point is on the valid gesture area */ - public boolean addPointOnKeyboard(final int x, final int y, final int time, + // TODO: Make this package private + public boolean addEventPoint(final int x, final int y, final int time, final boolean isMajorEvent) { final int size = getLength(); if (size <= 0) { - // Down event + // The first event of this stroke (a.k.a. down event). appendPoint(x, y, time); updateMajorEvent(x, y, time); } else { @@ -357,15 +302,18 @@ public class GestureStroke { } } + // TODO: Make this package private public final boolean hasRecognitionTimePast( final long currentTime, final long lastRecognitionTime) { - return currentTime > lastRecognitionTime + mParams.mRecognitionMinimumTime; + return currentTime > lastRecognitionTime + mRecognitionParams.mRecognitionMinimumTime; } + // TODO: Make this package private public final void appendAllBatchPoints(final InputPointers out) { appendBatchPoints(out, getLength()); } + // TODO: Make this package private public final void appendIncrementalBatchPoints(final InputPointers out) { appendBatchPoints(out, mIncrementalRecognitionSize); } @@ -381,10 +329,6 @@ public class GestureStroke { } private static int getDistance(final int x1, final int y1, final int x2, final int y2) { - final int dx = x1 - x2; - final int dy = y1 - y2; - // Note that, in recent versions of Android, FloatMath is actually slower than - // java.lang.Math due to the way the JIT optimizes java.lang.Math. - return (int)Math.sqrt(dx * dx + dy * dy); + return (int)Math.hypot(x1 - x2, y1 - y2); } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureTrailDrawingParams.java b/java/src/com/android/inputmethod/keyboard/internal/GestureTrailDrawingParams.java new file mode 100644 index 000000000..088f03aa6 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/GestureTrailDrawingParams.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.internal; + +import android.content.res.TypedArray; + +import com.android.inputmethod.latin.R; + +/** + * This class holds parameters to control how a gesture trail is drawn and animated on the screen. + * + * On the other hand, {@link GestureStrokeDrawingParams} class controls how each gesture stroke is + * sampled and interpolated. This class controls how those gesture strokes are displayed as a + * gesture trail and animated on the screen. + * + * @attr ref R.styleable#MainKeyboardView_gestureTrailFadeoutStartDelay + * @attr ref R.styleable#MainKeyboardView_gestureTrailFadeoutDuration + * @attr ref R.styleable#MainKeyboardView_gestureTrailUpdateInterval + * @attr ref R.styleable#MainKeyboardView_gestureTrailColor + * @attr ref R.styleable#MainKeyboardView_gestureTrailWidth + */ +final class GestureTrailDrawingParams { + private static final int FADEOUT_START_DELAY_FOR_DEBUG = 2000; // millisecond + private static final int FADEOUT_DURATION_FOR_DEBUG = 200; // millisecond + + public final int mTrailColor; + public final float mTrailStartWidth; + public final float mTrailEndWidth; + public final float mTrailBodyRatio; + public boolean mTrailShadowEnabled; + public final float mTrailShadowRatio; + public final int mFadeoutStartDelay; + public final int mFadeoutDuration; + public final int mUpdateInterval; + + public final int mTrailLingerDuration; + + public GestureTrailDrawingParams(final TypedArray mainKeyboardViewAttr) { + mTrailColor = mainKeyboardViewAttr.getColor( + R.styleable.MainKeyboardView_gestureTrailColor, 0); + mTrailStartWidth = mainKeyboardViewAttr.getDimension( + R.styleable.MainKeyboardView_gestureTrailStartWidth, 0.0f); + mTrailEndWidth = mainKeyboardViewAttr.getDimension( + R.styleable.MainKeyboardView_gestureTrailEndWidth, 0.0f); + final int PERCENTAGE_INT = 100; + mTrailBodyRatio = (float)mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_gestureTrailBodyRatio, PERCENTAGE_INT) + / (float)PERCENTAGE_INT; + final int trailShadowRatioInt = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_gestureTrailShadowRatio, 0); + mTrailShadowEnabled = (trailShadowRatioInt > 0); + mTrailShadowRatio = (float)trailShadowRatioInt / (float)PERCENTAGE_INT; + mFadeoutStartDelay = GestureTrailDrawingPoints.DEBUG_SHOW_POINTS + ? FADEOUT_START_DELAY_FOR_DEBUG + : mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_gestureTrailFadeoutStartDelay, 0); + mFadeoutDuration = GestureTrailDrawingPoints.DEBUG_SHOW_POINTS + ? FADEOUT_DURATION_FOR_DEBUG + : mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_gestureTrailFadeoutDuration, 0); + mTrailLingerDuration = mFadeoutStartDelay + mFadeoutDuration; + mUpdateInterval = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_gestureTrailUpdateInterval, 0); + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureTrail.java b/java/src/com/android/inputmethod/keyboard/internal/GestureTrailDrawingPoints.java index aca667919..bf4c4da10 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/GestureTrail.java +++ b/java/src/com/android/inputmethod/keyboard/internal/GestureTrailDrawingPoints.java @@ -16,7 +16,6 @@ package com.android.inputmethod.keyboard.internal; -import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; @@ -25,24 +24,22 @@ import android.graphics.Rect; import android.os.SystemClock; import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.utils.ResizableIntArray; -/* - * @attr ref R.styleable#MainKeyboardView_gestureTrailFadeoutStartDelay - * @attr ref R.styleable#MainKeyboardView_gestureTrailFadeoutDuration - * @attr ref R.styleable#MainKeyboardView_gestureTrailUpdateInterval - * @attr ref R.styleable#MainKeyboardView_gestureTrailColor - * @attr ref R.styleable#MainKeyboardView_gestureTrailWidth +/** + * This class holds drawing points to represent a gesture trail. The gesture trail may contain + * multiple non-contiguous gesture strokes and will be animated asynchronously from gesture input. + * + * On the other hand, {@link GestureStrokeDrawingPoints} class holds drawing points of each gesture + * stroke. This class holds drawing points of those gesture strokes to draw as a gesture trail. + * Drawing points in this class will be asynchronously removed when fading out animation goes. */ -final class GestureTrail { +final class GestureTrailDrawingPoints { public static final boolean DEBUG_SHOW_POINTS = false; public static final int POINT_TYPE_SAMPLED = 1; public static final int POINT_TYPE_INTERPOLATED = 2; - private static final int FADEOUT_START_DELAY_FOR_DEBUG = 2000; // millisecond - private static final int FADEOUT_DURATION_FOR_DEBUG = 200; // millisecond - private static final int DEFAULT_CAPACITY = GestureStrokeWithPreviewPoints.PREVIEW_CAPACITY; + private static final int DEFAULT_CAPACITY = GestureStrokeDrawingPoints.PREVIEW_CAPACITY; // These three {@link ResizableIntArray}s should be synchronized by {@link #mEventTimes}. private final ResizableIntArray mXCoordinates = new ResizableIntArray(DEFAULT_CAPACITY); @@ -56,46 +53,6 @@ final class GestureTrail { private int mTrailStartIndex; private int mLastInterpolatedDrawIndex; - static final class Params { - public final int mTrailColor; - public final float mTrailStartWidth; - public final float mTrailEndWidth; - public final float mTrailBodyRatio; - public boolean mTrailShadowEnabled; - public final float mTrailShadowRatio; - public final int mFadeoutStartDelay; - public final int mFadeoutDuration; - public final int mUpdateInterval; - - public final int mTrailLingerDuration; - - public Params(final TypedArray mainKeyboardViewAttr) { - mTrailColor = mainKeyboardViewAttr.getColor( - R.styleable.MainKeyboardView_gestureTrailColor, 0); - mTrailStartWidth = mainKeyboardViewAttr.getDimension( - R.styleable.MainKeyboardView_gestureTrailStartWidth, 0.0f); - mTrailEndWidth = mainKeyboardViewAttr.getDimension( - R.styleable.MainKeyboardView_gestureTrailEndWidth, 0.0f); - final int PERCENTAGE_INT = 100; - mTrailBodyRatio = (float)mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_gestureTrailBodyRatio, PERCENTAGE_INT) - / (float)PERCENTAGE_INT; - final int trailShadowRatioInt = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_gestureTrailShadowRatio, 0); - mTrailShadowEnabled = (trailShadowRatioInt > 0); - mTrailShadowRatio = (float)trailShadowRatioInt / (float)PERCENTAGE_INT; - mFadeoutStartDelay = DEBUG_SHOW_POINTS ? FADEOUT_START_DELAY_FOR_DEBUG - : mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_gestureTrailFadeoutStartDelay, 0); - mFadeoutDuration = DEBUG_SHOW_POINTS ? FADEOUT_DURATION_FOR_DEBUG - : mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_gestureTrailFadeoutDuration, 0); - mTrailLingerDuration = mFadeoutStartDelay + mFadeoutDuration; - mUpdateInterval = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_gestureTrailUpdateInterval, 0); - } - } - // Use this value as imaginary zero because x-coordinates may be zero. private static final int DOWN_EVENT_MARKER = -128; @@ -112,13 +69,13 @@ final class GestureTrail { ? DOWN_EVENT_MARKER - xCoordOrMark : xCoordOrMark; } - public void addStroke(final GestureStrokeWithPreviewPoints stroke, final long downTime) { + public void addStroke(final GestureStrokeDrawingPoints stroke, final long downTime) { synchronized (mEventTimes) { addStrokeLocked(stroke, downTime); } } - private void addStrokeLocked(final GestureStrokeWithPreviewPoints stroke, final long downTime) { + private void addStrokeLocked(final GestureStrokeDrawingPoints stroke, final long downTime) { final int trailSize = mEventTimes.getLength(); stroke.appendPreviewStroke(mEventTimes, mXCoordinates, mYCoordinates, mPointTypes); if (mEventTimes.getLength() == trailSize) { @@ -126,13 +83,14 @@ final class GestureTrail { } final int[] eventTimes = mEventTimes.getPrimitiveArray(); final int strokeId = stroke.getGestureStrokeId(); - // Because interpolation algorithm in {@link GestureStrokeWithPreviewPoints} can't determine + // Because interpolation algorithm in {@link GestureStrokeDrawingPoints} can't determine // the interpolated points in the last segment of gesture stroke, it may need recalculation // of interpolation when new segments are added to the stroke. // {@link #mLastInterpolatedDrawIndex} holds the start index of the last segment. It may // be updated by the interpolation - // {@link GestureStrokeWithPreviewPoints#interpolatePreviewStroke} - // or by animation {@link #drawGestureTrail(Canvas,Paint,Rect,Params)} below. + // {@link GestureStrokeDrawingPoints#interpolatePreviewStroke} + // or by animation {@link #drawGestureTrail(Canvas,Paint,Rect,GestureTrailDrawingParams)} + // below. final int lastInterpolatedIndex = (strokeId == mCurrentStrokeId) ? mLastInterpolatedDrawIndex : trailSize; mLastInterpolatedDrawIndex = stroke.interpolateStrokeAndReturnStartIndexOfLastSegment( @@ -161,7 +119,7 @@ final class GestureTrail { * @param params gesture trail display parameters * @return the width of a gesture trail */ - private static int getAlpha(final int elapsedTime, final Params params) { + private static int getAlpha(final int elapsedTime, final GestureTrailDrawingParams params) { if (elapsedTime < params.mFadeoutStartDelay) { return Constants.Color.ALPHA_OPAQUE; } @@ -180,7 +138,7 @@ final class GestureTrail { * @param params gesture trail display parameters * @return the width of a gesture trail */ - private static float getWidth(final int elapsedTime, final Params params) { + private static float getWidth(final int elapsedTime, final GestureTrailDrawingParams params) { final float deltaWidth = params.mTrailStartWidth - params.mTrailEndWidth; return params.mTrailStartWidth - (deltaWidth * elapsedTime) / params.mTrailLingerDuration; } @@ -197,14 +155,14 @@ final class GestureTrail { * @return true if some gesture trails remain to be drawn */ public boolean drawGestureTrail(final Canvas canvas, final Paint paint, - final Rect outBoundsRect, final Params params) { + final Rect outBoundsRect, final GestureTrailDrawingParams params) { synchronized (mEventTimes) { return drawGestureTrailLocked(canvas, paint, outBoundsRect, params); } } private boolean drawGestureTrailLocked(final Canvas canvas, final Paint paint, - final Rect outBoundsRect, final Params params) { + final Rect outBoundsRect, final GestureTrailDrawingParams params) { // Initialize bounds rectangle. outBoundsRect.setEmpty(); final int trailSize = mEventTimes.getLength(); diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureTrailsPreview.java b/java/src/com/android/inputmethod/keyboard/internal/GestureTrailsDrawingPreview.java index 19e995548..f7bd7efe0 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/GestureTrailsPreview.java +++ b/java/src/com/android/inputmethod/keyboard/internal/GestureTrailsDrawingPreview.java @@ -24,21 +24,17 @@ import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; -import android.os.Message; +import android.os.Handler; import android.util.SparseArray; -import android.view.View; import com.android.inputmethod.keyboard.PointerTracker; -import com.android.inputmethod.keyboard.internal.GestureTrail.Params; -import com.android.inputmethod.latin.utils.CollectionUtils; -import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper; /** - * Draw gesture trail preview graphics during gesture. + * Draw preview graphics of multiple gesture trails during gesture input. */ -public final class GestureTrailsPreview extends AbstractDrawingPreview { - private final SparseArray<GestureTrail> mGestureTrails = CollectionUtils.newSparseArray(); - private final Params mGestureTrailParams; +public final class GestureTrailsDrawingPreview extends AbstractDrawingPreview implements Runnable { + private final SparseArray<GestureTrailDrawingPoints> mGestureTrails = new SparseArray<>(); + private final GestureTrailDrawingParams mDrawingParams; private final Paint mGesturePaint; private int mOffscreenWidth; private int mOffscreenHeight; @@ -49,42 +45,10 @@ public final class GestureTrailsPreview extends AbstractDrawingPreview { private final Rect mDirtyRect = new Rect(); private final Rect mGestureTrailBoundsRect = new Rect(); // per trail - private final DrawingHandler mDrawingHandler; + private final Handler mDrawingHandler = new Handler(); - private static final class DrawingHandler - extends StaticInnerHandlerWrapper<GestureTrailsPreview> { - private static final int MSG_UPDATE_GESTURE_TRAIL = 0; - - private final Params mGestureTrailParams; - - public DrawingHandler(final GestureTrailsPreview outerInstance, - final Params gestureTrailParams) { - super(outerInstance); - mGestureTrailParams = gestureTrailParams; - } - - @Override - public void handleMessage(final Message msg) { - final GestureTrailsPreview preview = getOuterInstance(); - if (preview == null) return; - switch (msg.what) { - case MSG_UPDATE_GESTURE_TRAIL: - preview.getDrawingView().invalidate(); - break; - } - } - - public void postUpdateGestureTrailPreview() { - removeMessages(MSG_UPDATE_GESTURE_TRAIL); - sendMessageDelayed(obtainMessage(MSG_UPDATE_GESTURE_TRAIL), - mGestureTrailParams.mUpdateInterval); - } - } - - public GestureTrailsPreview(final View drawingView, final TypedArray mainKeyboardViewAttr) { - super(drawingView); - mGestureTrailParams = new Params(mainKeyboardViewAttr); - mDrawingHandler = new DrawingHandler(this, mGestureTrailParams); + public GestureTrailsDrawingPreview(final TypedArray mainKeyboardViewAttr) { + mDrawingParams = new GestureTrailDrawingParams(mainKeyboardViewAttr); final Paint gesturePaint = new Paint(); gesturePaint.setAntiAlias(true); gesturePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); @@ -92,19 +56,17 @@ public final class GestureTrailsPreview extends AbstractDrawingPreview { } @Override - public void setKeyboardGeometry(final int[] originCoords, final int width, final int height) { - mOffscreenOffsetY = (int)( - height * GestureStroke.EXTRA_GESTURE_TRAIL_AREA_ABOVE_KEYBOARD_RATIO); + public void setKeyboardViewGeometry(final int[] originCoords, final int width, + final int height) { + super.setKeyboardViewGeometry(originCoords, width, height); + mOffscreenOffsetY = (int)(height + * GestureStrokeRecognitionPoints.EXTRA_GESTURE_TRAIL_AREA_ABOVE_KEYBOARD_RATIO); mOffscreenWidth = width; mOffscreenHeight = mOffscreenOffsetY + height; } @Override - public void onDetachFromWindow() { - freeOffscreenBuffer(); - } - - public void deallocateMemory() { + public void onDeallocateMemory() { freeOffscreenBuffer(); } @@ -144,9 +106,9 @@ public final class GestureTrailsPreview extends AbstractDrawingPreview { // Trails count == fingers count that have ever been active. final int trailsCount = mGestureTrails.size(); for (int index = 0; index < trailsCount; index++) { - final GestureTrail trail = mGestureTrails.valueAt(index); + final GestureTrailDrawingPoints trail = mGestureTrails.valueAt(index); needsUpdatingGestureTrail |= trail.drawGestureTrail(offscreenCanvas, paint, - mGestureTrailBoundsRect, mGestureTrailParams); + mGestureTrailBoundsRect, mDrawingParams); // {@link #mGestureTrailBoundsRect} has bounding box of the trail. dirtyRect.union(mGestureTrailBoundsRect); } @@ -154,6 +116,12 @@ public final class GestureTrailsPreview extends AbstractDrawingPreview { return needsUpdatingGestureTrail; } + @Override + public void run() { + // Update preview. + invalidateDrawingView(); + } + /** * Draws the preview * @param canvas The canvas where the preview is drawn. @@ -168,7 +136,8 @@ public final class GestureTrailsPreview extends AbstractDrawingPreview { final boolean needsUpdatingGestureTrail = drawGestureTrails( mOffscreenCanvas, mGesturePaint, mDirtyRect); if (needsUpdatingGestureTrail) { - mDrawingHandler.postUpdateGestureTrailPreview(); + mDrawingHandler.removeCallbacks(this); + mDrawingHandler.postDelayed(this, mDrawingParams.mUpdateInterval); } // Transfer offscreen buffer to screen. if (!mDirtyRect.isEmpty()) { @@ -189,17 +158,17 @@ public final class GestureTrailsPreview extends AbstractDrawingPreview { if (!isPreviewEnabled()) { return; } - GestureTrail trail; + GestureTrailDrawingPoints trail; synchronized (mGestureTrails) { trail = mGestureTrails.get(tracker.mPointerId); if (trail == null) { - trail = new GestureTrail(); + trail = new GestureTrailDrawingPoints(); mGestureTrails.put(tracker.mPointerId, trail); } } - trail.addStroke(tracker.getGestureStrokeWithPreviewPoints(), tracker.getDownTime()); + trail.addStroke(tracker.getGestureStrokeDrawingPoints(), tracker.getDownTime()); // TODO: Should narrow the invalidate region. - getDrawingView().invalidate(); + invalidateDrawingView(); } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java index b528b692e..07ac06bab 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java @@ -16,7 +16,6 @@ package com.android.inputmethod.keyboard.internal; -import android.content.res.ColorStateList; import android.graphics.Typeface; import com.android.inputmethod.latin.utils.ResourceUtils; @@ -33,9 +32,10 @@ public final class KeyDrawParams { public int mHintLabelSize; public int mPreviewTextSize; - public ColorStateList mTextColorStateList; + public int mTextColor; public int mTextInactivatedColor; public int mTextShadowColor; + public int mFunctionalTextColor; public int mHintLetterColor; public int mHintLabelColor; public int mShiftedLetterHintInactivatedColor; @@ -58,9 +58,10 @@ public final class KeyDrawParams { mHintLabelSize = copyFrom.mHintLabelSize; mPreviewTextSize = copyFrom.mPreviewTextSize; - mTextColorStateList = copyFrom.mTextColorStateList; + mTextColor = copyFrom.mTextColor; mTextInactivatedColor = copyFrom.mTextInactivatedColor; mTextShadowColor = copyFrom.mTextShadowColor; + mFunctionalTextColor = copyFrom.mFunctionalTextColor; mHintLetterColor = copyFrom.mHintLetterColor; mHintLabelColor = copyFrom.mHintLabelColor; mShiftedLetterHintInactivatedColor = copyFrom.mShiftedLetterHintInactivatedColor; @@ -90,10 +91,11 @@ public final class KeyDrawParams { attr.mShiftedLetterHintRatio, mShiftedLetterHintSize); mHintLabelSize = selectTextSize(keyHeight, attr.mHintLabelRatio, mHintLabelSize); mPreviewTextSize = selectTextSize(keyHeight, attr.mPreviewTextRatio, mPreviewTextSize); - mTextColorStateList = - attr.mTextColorStateList != null ? attr.mTextColorStateList : mTextColorStateList; + + mTextColor = selectColor(attr.mTextColor, mTextColor); mTextInactivatedColor = selectColor(attr.mTextInactivatedColor, mTextInactivatedColor); mTextShadowColor = selectColor(attr.mTextShadowColor, mTextShadowColor); + mFunctionalTextColor = selectColor(attr.mFunctionalTextColor, mFunctionalTextColor); mHintLetterColor = selectColor(attr.mHintLetterColor, mHintLetterColor); mHintLabelColor = selectColor(attr.mHintLabelColor, mHintLabelColor); mShiftedLetterHintInactivatedColor = selectColor( diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewChoreographer.java b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewChoreographer.java new file mode 100644 index 000000000..cd29c8d17 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewChoreographer.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.internal; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; + +import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.latin.utils.CoordinateUtils; +import com.android.inputmethod.latin.utils.ViewLayoutUtils; + +import java.util.ArrayDeque; +import java.util.HashMap; +import java.util.HashSet; + +/** + * This class controls pop up key previews. This class decides: + * - what kind of key previews should be shown. + * - where key previews should be placed. + * - how key previews should be shown and dismissed. + */ +public final class KeyPreviewChoreographer { + // Free {@link KeyPreviewView} pool that can be used for key preview. + private final ArrayDeque<KeyPreviewView> mFreeKeyPreviewViews = new ArrayDeque<>(); + // Map from {@link Key} to {@link KeyPreviewView} that is currently being displayed as key + // preview. + private final HashMap<Key,KeyPreviewView> mShowingKeyPreviewViews = new HashMap<>(); + + private final KeyPreviewDrawParams mParams; + + public KeyPreviewChoreographer(final KeyPreviewDrawParams params) { + mParams = params; + } + + public KeyPreviewView getKeyPreviewView(final Key key, final ViewGroup placerView) { + KeyPreviewView keyPreviewView = mShowingKeyPreviewViews.remove(key); + if (keyPreviewView != null) { + return keyPreviewView; + } + keyPreviewView = mFreeKeyPreviewViews.poll(); + if (keyPreviewView != null) { + return keyPreviewView; + } + final Context context = placerView.getContext(); + keyPreviewView = new KeyPreviewView(context, null /* attrs */); + keyPreviewView.setBackgroundResource(mParams.mPreviewBackgroundResId); + placerView.addView(keyPreviewView, ViewLayoutUtils.newLayoutParam(placerView, 0, 0)); + return keyPreviewView; + } + + public boolean isShowingKeyPreview(final Key key) { + return mShowingKeyPreviewViews.containsKey(key); + } + + public void dismissAllKeyPreviews() { + for (final Key key : new HashSet<>(mShowingKeyPreviewViews.keySet())) { + dismissKeyPreview(key, false /* withAnimation */); + } + } + + public void dismissKeyPreview(final Key key, final boolean withAnimation) { + if (key == null) { + return; + } + final KeyPreviewView keyPreviewView = mShowingKeyPreviewViews.get(key); + if (keyPreviewView == null) { + return; + } + final Object tag = keyPreviewView.getTag(); + if (withAnimation) { + if (tag instanceof KeyPreviewAnimations) { + final KeyPreviewAnimations animation = (KeyPreviewAnimations)tag; + animation.startDismiss(); + return; + } + } + // Dismiss preview without animation. + mShowingKeyPreviewViews.remove(key); + if (tag instanceof Animator) { + ((Animator)tag).cancel(); + } + keyPreviewView.setTag(null); + keyPreviewView.setVisibility(View.INVISIBLE); + mFreeKeyPreviewViews.add(keyPreviewView); + } + + public void placeAndShowKeyPreview(final Key key, final KeyboardIconsSet iconsSet, + final KeyDrawParams drawParams, final int keyboardViewWidth, final int[] keyboardOrigin, + final ViewGroup placerView, final boolean withAnimation) { + final KeyPreviewView keyPreviewView = getKeyPreviewView(key, placerView); + placeKeyPreview( + key, keyPreviewView, iconsSet, drawParams, keyboardViewWidth, keyboardOrigin); + showKeyPreview(key, keyPreviewView, withAnimation); + } + + private void placeKeyPreview(final Key key, final KeyPreviewView keyPreviewView, + final KeyboardIconsSet iconsSet, final KeyDrawParams drawParams, + final int keyboardViewWidth, final int[] originCoords) { + keyPreviewView.setPreviewVisual(key, iconsSet, drawParams); + keyPreviewView.measure( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + mParams.setGeometry(keyPreviewView); + final int previewWidth = keyPreviewView.getMeasuredWidth(); + final int previewHeight = mParams.mPreviewHeight; + final int keyDrawWidth = key.getDrawWidth(); + // The key preview is horizontally aligned with the center of the visible part of the + // parent key. If it doesn't fit in this {@link KeyboardView}, it is moved inward to fit and + // the left/right background is used if such background is specified. + final int keyPreviewPosition; + int previewX = key.getDrawX() - (previewWidth - keyDrawWidth) / 2 + + CoordinateUtils.x(originCoords); + if (previewX < 0) { + previewX = 0; + keyPreviewPosition = KeyPreviewView.POSITION_LEFT; + } else if (previewX > keyboardViewWidth - previewWidth) { + previewX = keyboardViewWidth - previewWidth; + keyPreviewPosition = KeyPreviewView.POSITION_RIGHT; + } else { + keyPreviewPosition = KeyPreviewView.POSITION_MIDDLE; + } + final boolean hasMoreKeys = (key.getMoreKeys() != null); + keyPreviewView.setPreviewBackground(hasMoreKeys, keyPreviewPosition); + // The key preview is placed vertically above the top edge of the parent key with an + // arbitrary offset. + final int previewY = key.getY() - previewHeight + mParams.mPreviewOffset + + CoordinateUtils.y(originCoords); + + ViewLayoutUtils.placeViewAt( + keyPreviewView, previewX, previewY, previewWidth, previewHeight); + keyPreviewView.setPivotX(previewWidth / 2.0f); + keyPreviewView.setPivotY(previewHeight); + } + + private void showKeyPreview(final Key key, final KeyPreviewView keyPreviewView, + final boolean withAnimation) { + if (!withAnimation) { + keyPreviewView.setVisibility(View.VISIBLE); + mShowingKeyPreviewViews.put(key, keyPreviewView); + return; + } + + // Show preview with animation. + final Animator showUpAnimation = createShowUpAniation(key, keyPreviewView); + final Animator dismissAnimation = createDismissAnimation(key, keyPreviewView); + final KeyPreviewAnimations animation = new KeyPreviewAnimations( + showUpAnimation, dismissAnimation); + keyPreviewView.setTag(animation); + animation.startShowUp(); + } + + private static final float KEY_PREVIEW_SHOW_UP_END_SCALE = 1.0f; + private static final AccelerateInterpolator ACCELERATE_INTERPOLATOR = + new AccelerateInterpolator(); + private static final DecelerateInterpolator DECELERATE_INTERPOLATOR = + new DecelerateInterpolator(); + + private Animator createShowUpAniation(final Key key, final KeyPreviewView keyPreviewView) { + // TODO: Optimization for no scale animation and no duration. + final ObjectAnimator scaleXAnimation = ObjectAnimator.ofFloat( + keyPreviewView, View.SCALE_X, mParams.getShowUpStartScale(), + KEY_PREVIEW_SHOW_UP_END_SCALE); + final ObjectAnimator scaleYAnimation = ObjectAnimator.ofFloat( + keyPreviewView, View.SCALE_Y, mParams.getShowUpStartScale(), + KEY_PREVIEW_SHOW_UP_END_SCALE); + final AnimatorSet showUpAnimation = new AnimatorSet(); + showUpAnimation.play(scaleXAnimation).with(scaleYAnimation); + showUpAnimation.setDuration(mParams.getShowUpDuration()); + showUpAnimation.setInterpolator(DECELERATE_INTERPOLATOR); + showUpAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(final Animator animation) { + showKeyPreview(key, keyPreviewView, false /* withAnimation */); + } + }); + return showUpAnimation; + } + + private Animator createDismissAnimation(final Key key, final KeyPreviewView keyPreviewView) { + // TODO: Optimization for no scale animation and no duration. + final ObjectAnimator scaleXAnimation = ObjectAnimator.ofFloat( + keyPreviewView, View.SCALE_X, mParams.getDismissEndScale()); + final ObjectAnimator scaleYAnimation = ObjectAnimator.ofFloat( + keyPreviewView, View.SCALE_Y, mParams.getDismissEndScale()); + final AnimatorSet dismissAnimation = new AnimatorSet(); + dismissAnimation.play(scaleXAnimation).with(scaleYAnimation); + final int dismissDuration = Math.min( + mParams.getDismissDuration(), mParams.getLingerTimeout()); + dismissAnimation.setDuration(dismissDuration); + dismissAnimation.setInterpolator(ACCELERATE_INTERPOLATOR); + dismissAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(final Animator animation) { + dismissKeyPreview(key, false /* withAnimation */); + } + }); + return dismissAnimation; + } + + private static class KeyPreviewAnimations extends AnimatorListenerAdapter { + private final Animator mShowUpAnimation; + private final Animator mDismissAnimation; + + public KeyPreviewAnimations(final Animator showUpAnimation, + final Animator dismissAnimation) { + mShowUpAnimation = showUpAnimation; + mDismissAnimation = dismissAnimation; + } + + public void startShowUp() { + mShowUpAnimation.start(); + } + + public void startDismiss() { + if (mShowUpAnimation.isRunning()) { + mShowUpAnimation.addListener(this); + return; + } + mDismissAnimation.start(); + } + + @Override + public void onAnimationEnd(final Animator animation) { + mDismissAnimation.start(); + } + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java index 609d1a57f..68c9831fa 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java @@ -16,7 +16,23 @@ package com.android.inputmethod.keyboard.internal; +import android.content.res.TypedArray; +import android.view.View; + +import com.android.inputmethod.latin.R; + public final class KeyPreviewDrawParams { + // XML attributes of {@link MainKeyboardView}. + public final int mPreviewOffset; + public final int mPreviewHeight; + public final int mPreviewBackgroundResId; + private int mShowUpDuration; + private int mDismissDuration; + private float mShowUpStartScale; + private float mDismissEndScale; + private int mLingerTimeout; + private boolean mShowPopup = true; + // The graphical geometry of the key preview. // <-width-> // +-------+ ^ @@ -34,11 +50,89 @@ public final class KeyPreviewDrawParams { // paddings. To align the more keys keyboard panel's visible part with the visible part of // the background, we need to record the width and height of key preview that don't include // invisible paddings. - public int mPreviewVisibleWidth; - public int mPreviewVisibleHeight; + private int mVisibleWidth; + private int mVisibleHeight; // The key preview may have an arbitrary offset and its background that may have a bottom // padding. To align the more keys keyboard and the key preview we also need to record the // offset between the top edge of parent key and the bottom of the visible part of key // preview background. - public int mPreviewVisibleOffset; + private int mVisibleOffset; + + public KeyPreviewDrawParams(final TypedArray mainKeyboardViewAttr) { + mPreviewOffset = mainKeyboardViewAttr.getDimensionPixelOffset( + R.styleable.MainKeyboardView_keyPreviewOffset, 0); + mPreviewHeight = mainKeyboardViewAttr.getDimensionPixelSize( + R.styleable.MainKeyboardView_keyPreviewHeight, 0); + mPreviewBackgroundResId = mainKeyboardViewAttr.getResourceId( + R.styleable.MainKeyboardView_keyPreviewBackground, 0); + mLingerTimeout = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_keyPreviewLingerTimeout, 0); + } + + public void setVisibleOffset(final int previewVisibleOffset) { + mVisibleOffset = previewVisibleOffset; + } + + public int getVisibleOffset() { + return mVisibleOffset; + } + + public void setGeometry(final View previewTextView) { + final int previewWidth = previewTextView.getMeasuredWidth(); + final int previewHeight = mPreviewHeight; + // The width and height of visible part of the key preview background. The content marker + // of the background 9-patch have to cover the visible part of the background. + mVisibleWidth = previewWidth - previewTextView.getPaddingLeft() + - previewTextView.getPaddingRight(); + mVisibleHeight = previewHeight - previewTextView.getPaddingTop() + - previewTextView.getPaddingBottom(); + // The distance between the top edge of the parent key and the bottom of the visible part + // of the key preview background. + setVisibleOffset(mPreviewOffset - previewTextView.getPaddingBottom()); + } + + public int getVisibleWidth() { + return mVisibleWidth; + } + + public int getVisibleHeight() { + return mVisibleHeight; + } + + public void setPopupEnabled(final boolean enabled, final int lingerTimeout) { + mShowPopup = enabled; + mLingerTimeout = lingerTimeout; + } + + public boolean isPopupEnabled() { + return mShowPopup; + } + + public int getLingerTimeout() { + return mLingerTimeout; + } + + public void setAnimationParams(final float showUpStartScale, final int showUpDuration, + final float dismissEndScale, final int dismissDuration) { + mShowUpStartScale = showUpStartScale; + mShowUpDuration = showUpDuration; + mDismissEndScale = dismissEndScale; + mDismissDuration = dismissDuration; + } + + public float getShowUpStartScale() { + return mShowUpStartScale; + } + + public int getShowUpDuration() { + return mShowUpDuration; + } + + public float getDismissEndScale() { + return mDismissEndScale; + } + + public int getDismissDuration() { + return mDismissDuration; + } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewView.java b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewView.java new file mode 100644 index 000000000..360faf829 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewView.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.internal; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.widget.TextView; + +import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.latin.R; + +/** + * The pop up key preview view. + */ +public class KeyPreviewView extends TextView { + public static final int POSITION_MIDDLE = 0; + public static final int POSITION_LEFT = 1; + public static final int POSITION_RIGHT = 2; + + public KeyPreviewView(final Context context, final AttributeSet attrs) { + this(context, attrs, 0); + } + + public KeyPreviewView(final Context context, final AttributeSet attrs, final int defStyleAttr) { + super(context, attrs, defStyleAttr); + setGravity(Gravity.CENTER); + } + + public void setPreviewVisual(final Key key, final KeyboardIconsSet iconsSet, + final KeyDrawParams drawParams) { + // What we show as preview should match what we show on a key top in onDraw(). + final int iconId = key.getIconId(); + if (iconId != KeyboardIconsSet.ICON_UNDEFINED) { + setCompoundDrawables(null, null, null, key.getPreviewIcon(iconsSet)); + setText(null); + return; + } + + setCompoundDrawables(null, null, null, null); + setTextColor(drawParams.mPreviewTextColor); + setTextSize(TypedValue.COMPLEX_UNIT_PX, key.selectPreviewTextSize(drawParams)); + setTypeface(key.selectPreviewTypeface(drawParams)); + // TODO Should take care of temporaryShiftLabel here. + setText(key.getPreviewLabel()); + } + + // Background state set + private static final int[][][] KEY_PREVIEW_BACKGROUND_STATE_TABLE = { + { // POSITION_MIDDLE + {}, + { R.attr.state_has_morekeys } + }, + { // POSITION_LEFT + { R.attr.state_left_edge }, + { R.attr.state_left_edge, R.attr.state_has_morekeys } + }, + { // POSITION_RIGHT + { R.attr.state_right_edge }, + { R.attr.state_right_edge, R.attr.state_has_morekeys } + } + }; + private static final int STATE_NORMAL = 0; + private static final int STATE_HAS_MOREKEYS = 1; + + public void setPreviewBackground(final boolean hasMoreKeys, final int position) { + final Drawable background = getBackground(); + if (background == null) { + return; + } + final int hasMoreKeysState = hasMoreKeys ? STATE_HAS_MOREKEYS : STATE_NORMAL; + background.setState(KEY_PREVIEW_BACKGROUND_STATE_TABLE[position][hasMoreKeysState]); + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java index 22f5b3dd1..48ba8e051 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java @@ -19,114 +19,54 @@ package com.android.inputmethod.keyboard.internal; import static com.android.inputmethod.latin.Constants.CODE_OUTPUT_TEXT; import static com.android.inputmethod.latin.Constants.CODE_UNSPECIFIED; -import android.text.TextUtils; - import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.LatinImeLogger; -import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.StringUtils; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Locale; - /** - * The string parser of more keys specification. - * The specification is comma separated texts each of which represents one "more key". - * The specification might have label or string resource reference in it. These references are - * expanded before parsing comma. - * - Label reference should be a string representation of label (!text/label_name) - * - String resource reference should be a string representation of resource (!text/resource_name) - * Each "more key" specification is one of the following: - * - Label optionally followed by keyOutputText or code (keyLabel|keyOutputText). - * - Icon followed by keyOutputText or code (!icon/icon_name|!code/code_name) - * - Icon should be a string representation of icon (!icon/icon_name). - * - Code should be a code point presented by hexadecimal string prefixed with "0x", or a string - * representation of code (!code/code_name). + * The string parser of the key specification. + * + * Each key specification is one of the following: + * - Label optionally followed by keyOutputText (keyLabel|keyOutputText). + * - Label optionally followed by code point (keyLabel|!code/code_name). + * - Icon followed by keyOutputText (!icon/icon_name|keyOutputText). + * - Icon followed by code point (!icon/icon_name|!code/code_name). + * Label and keyOutputText are one of the following: + * - Literal string. + * - Label reference represented by (!text/label_name), see {@link KeyboardTextsSet}. + * - String resource reference represented by (!text/resource_name), see {@link KeyboardTextsSet}. + * Icon is represented by (!icon/icon_name), see {@link KeyboardIconsSet}. + * Code is one of the following: + * - Code point presented by hexadecimal string prefixed with "0x" + * - Code reference represented by (!code/code_name), see {@link KeyboardCodesSet}. * Special character, comma ',' backslash '\', and bar '|' can be escaped by '\' character. - * Note that the '\' is also parsed by XML parser and CSV parser as well. - * See {@link KeyboardIconsSet} about icon_name. + * Note that the '\' is also parsed by XML parser and {@link MoreKeySpec#splitKeySpecs(String)} + * as well. */ +// TODO: Rename to KeySpec and make this class to the key specification object. public final class KeySpecParser { - private static final boolean DEBUG = LatinImeLogger.sDBG; - - private static final int MAX_STRING_REFERENCE_INDIRECTION = 10; - // Constants for parsing. - private static final char COMMA = ','; - private static final char BACKSLASH = '\\'; - private static final char VERTICAL_BAR = '|'; - private static final String PREFIX_TEXT = "!text/"; - static final String PREFIX_ICON = "!icon/"; - private static final String PREFIX_CODE = "!code/"; + private static final char BACKSLASH = Constants.CODE_BACKSLASH; + private static final char VERTICAL_BAR = Constants.CODE_VERTICAL_BAR; private static final String PREFIX_HEX = "0x"; - private static final String ADDITIONAL_MORE_KEY_MARKER = "%"; private KeySpecParser() { // Intentional empty constructor for utility class. } - /** - * Split the text containing multiple key specifications separated by commas into an array of - * key specifications. - * A key specification can contain a character escaped by the backslash character, including a - * comma character. - * Note that an empty key specification will be eliminated from the result array. - * - * @param text the text containing multiple key specifications. - * @return an array of key specification text. Null if the specified <code>text</code> is empty - * or has no key specifications. - */ - public static String[] splitKeySpecs(final String text) { - final int size = text.length(); - if (size == 0) { - return null; - } - // Optimization for one-letter key specification. - if (size == 1) { - return text.charAt(0) == COMMA ? null : new String[] { text }; - } + private static boolean hasIcon(final String keySpec) { + return keySpec.startsWith(KeyboardIconsSet.PREFIX_ICON); + } - ArrayList<String> list = null; - int start = 0; - // The characters in question in this loop are COMMA and BACKSLASH. These characters never - // match any high or low surrogate character. So it is OK to iterate through with char - // index. - for (int pos = 0; pos < size; pos++) { - final char c = text.charAt(pos); - if (c == COMMA) { - // Skip empty entry. - if (pos - start > 0) { - if (list == null) { - list = CollectionUtils.newArrayList(); - } - list.add(text.substring(start, pos)); - } - // Skip comma - start = pos + 1; - } else if (c == BACKSLASH) { - // Skip escape character and escaped character. - pos++; - } - } - final String remain = (size - start > 0) ? text.substring(start) : null; - if (list == null) { - return remain != null ? new String[] { remain } : null; + private static boolean hasCode(final String keySpec, final int labelEnd) { + if (labelEnd <= 0 || labelEnd + 1 >= keySpec.length()) { + return false; } - if (remain != null) { - list.add(remain); + if (keySpec.startsWith(KeyboardCodesSet.PREFIX_CODE, labelEnd + 1)) { + return true; } - return list.toArray(new String[list.size()]); - } - - private static boolean hasIcon(final String moreKeySpec) { - return moreKeySpec.startsWith(PREFIX_ICON); - } - - private static boolean hasCode(final String moreKeySpec) { - final int end = indexOfLabelEnd(moreKeySpec, 0); - if (end > 0 && end + 1 < moreKeySpec.length() && moreKeySpec.startsWith( - PREFIX_CODE, end + 1)) { + // This is a workaround to have a key that has a supplementary code point. We can't put a + // string in resource as a XML entity of a supplementary code point or a surrogate pair. + if (keySpec.startsWith(PREFIX_HEX, labelEnd + 1)) { return true; } return false; @@ -151,17 +91,21 @@ public final class KeySpecParser { return sb.toString(); } - private static int indexOfLabelEnd(final String moreKeySpec, final int start) { - if (moreKeySpec.indexOf(BACKSLASH, start) < 0) { - final int end = moreKeySpec.indexOf(VERTICAL_BAR, start); - if (end == 0) { - throw new KeySpecParserError(VERTICAL_BAR + " at " + start + ": " + moreKeySpec); + private static int indexOfLabelEnd(final String keySpec) { + final int length = keySpec.length(); + if (keySpec.indexOf(BACKSLASH) < 0) { + final int labelEnd = keySpec.indexOf(VERTICAL_BAR); + if (labelEnd == 0) { + if (length == 1) { + // Treat a sole vertical bar as a special case of key label. + return -1; + } + throw new KeySpecParserError("Empty label"); } - return end; + return labelEnd; } - final int length = moreKeySpec.length(); - for (int pos = start; pos < length; pos++) { - final char c = moreKeySpec.charAt(pos); + for (int pos = 0; pos < length; pos++) { + final char c = keySpec.charAt(pos); if (c == BACKSLASH && pos + 1 < length) { // Skip escape char pos++; @@ -172,63 +116,85 @@ public final class KeySpecParser { return -1; } - public static String getLabel(final String moreKeySpec) { - if (hasIcon(moreKeySpec)) { + private static String getBeforeLabelEnd(final String keySpec, final int labelEnd) { + return (labelEnd < 0) ? keySpec : keySpec.substring(0, labelEnd); + } + + private static String getAfterLabelEnd(final String keySpec, final int labelEnd) { + return keySpec.substring(labelEnd + /* VERTICAL_BAR */1); + } + + private static void checkDoubleLabelEnd(final String keySpec, final int labelEnd) { + if (indexOfLabelEnd(getAfterLabelEnd(keySpec, labelEnd)) < 0) { + return; + } + throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + keySpec); + } + + public static String getLabel(final String keySpec) { + if (keySpec == null) { + // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory. + return null; + } + if (hasIcon(keySpec)) { return null; } - final int end = indexOfLabelEnd(moreKeySpec, 0); - final String label = (end > 0) ? parseEscape(moreKeySpec.substring(0, end)) - : parseEscape(moreKeySpec); - if (TextUtils.isEmpty(label)) { - throw new KeySpecParserError("Empty label: " + moreKeySpec); + final int labelEnd = indexOfLabelEnd(keySpec); + final String label = parseEscape(getBeforeLabelEnd(keySpec, labelEnd)); + if (label.isEmpty()) { + throw new KeySpecParserError("Empty label: " + keySpec); } return label; } - private static String getOutputTextInternal(final String moreKeySpec) { - final int end = indexOfLabelEnd(moreKeySpec, 0); - if (end <= 0) { + private static String getOutputTextInternal(final String keySpec, final int labelEnd) { + if (labelEnd <= 0) { return null; } - if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) { - throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + moreKeySpec); - } - return parseEscape(moreKeySpec.substring(end + /* VERTICAL_BAR */1)); + checkDoubleLabelEnd(keySpec, labelEnd); + return parseEscape(getAfterLabelEnd(keySpec, labelEnd)); } - static String getOutputText(final String moreKeySpec) { - if (hasCode(moreKeySpec)) { + public static String getOutputText(final String keySpec) { + if (keySpec == null) { + // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory. + return null; + } + final int labelEnd = indexOfLabelEnd(keySpec); + if (hasCode(keySpec, labelEnd)) { return null; } - final String outputText = getOutputTextInternal(moreKeySpec); + final String outputText = getOutputTextInternal(keySpec, labelEnd); if (outputText != null) { if (StringUtils.codePointCount(outputText) == 1) { // If output text is one code point, it should be treated as a code. // See {@link #getCode(Resources, String)}. return null; } - if (!TextUtils.isEmpty(outputText)) { - return outputText; + if (outputText.isEmpty()) { + throw new KeySpecParserError("Empty outputText: " + keySpec); } - throw new KeySpecParserError("Empty outputText: " + moreKeySpec); + return outputText; } - final String label = getLabel(moreKeySpec); + final String label = getLabel(keySpec); if (label == null) { - throw new KeySpecParserError("Empty label: " + moreKeySpec); + throw new KeySpecParserError("Empty label: " + keySpec); } // Code is automatically generated for one letter label. See {@link getCode()}. return (StringUtils.codePointCount(label) == 1) ? null : label; } - static int getCode(final String moreKeySpec, final KeyboardCodesSet codesSet) { - if (hasCode(moreKeySpec)) { - final int end = indexOfLabelEnd(moreKeySpec, 0); - if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) { - throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + moreKeySpec); - } - return parseCode(moreKeySpec.substring(end + 1), codesSet, CODE_UNSPECIFIED); + public static int getCode(final String keySpec) { + if (keySpec == null) { + // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory. + return CODE_UNSPECIFIED; + } + final int labelEnd = indexOfLabelEnd(keySpec); + if (hasCode(keySpec, labelEnd)) { + checkDoubleLabelEnd(keySpec, labelEnd); + return parseCode(getAfterLabelEnd(keySpec, labelEnd), CODE_UNSPECIFIED); } - final String outputText = getOutputTextInternal(moreKeySpec); + final String outputText = getOutputTextInternal(keySpec, labelEnd); if (outputText != null) { // If output text is one code point, it should be treated as a code. // See {@link #getOutputText(String)}. @@ -237,138 +203,41 @@ public final class KeySpecParser { } return CODE_OUTPUT_TEXT; } - final String label = getLabel(moreKeySpec); - // Code is automatically generated for one letter label. - if (StringUtils.codePointCount(label) == 1) { - return label.codePointAt(0); - } - return CODE_OUTPUT_TEXT; - } - - public static int parseCode(final String text, final KeyboardCodesSet codesSet, - final int defCode) { - if (text == null) return defCode; - if (text.startsWith(PREFIX_CODE)) { - return codesSet.getCode(text.substring(PREFIX_CODE.length())); - } else if (text.startsWith(PREFIX_HEX)) { - return Integer.parseInt(text.substring(PREFIX_HEX.length()), 16); - } else { - return Integer.parseInt(text); - } - } - - public static int getIconId(final String moreKeySpec) { - if (moreKeySpec != null && hasIcon(moreKeySpec)) { - final int end = moreKeySpec.indexOf(VERTICAL_BAR, PREFIX_ICON.length()); - final String name = (end < 0) ? moreKeySpec.substring(PREFIX_ICON.length()) - : moreKeySpec.substring(PREFIX_ICON.length(), end); - return KeyboardIconsSet.getIconId(name); - } - return KeyboardIconsSet.ICON_UNDEFINED; - } - - private static <T> ArrayList<T> arrayAsList(final T[] array, final int start, final int end) { - if (array == null) { - throw new NullPointerException(); - } - if (start < 0 || start > end || end > array.length) { - throw new IllegalArgumentException(); - } - - final ArrayList<T> list = CollectionUtils.newArrayList(end - start); - for (int i = start; i < end; i++) { - list.add(array[i]); + final String label = getLabel(keySpec); + if (label == null) { + throw new KeySpecParserError("Empty label: " + keySpec); } - return list; + // Code is automatically generated for one letter label. + return (StringUtils.codePointCount(label) == 1) ? label.codePointAt(0) : CODE_OUTPUT_TEXT; } - private static final String[] EMPTY_STRING_ARRAY = new String[0]; - - private static String[] filterOutEmptyString(final String[] array) { - if (array == null) { - return EMPTY_STRING_ARRAY; + public static int parseCode(final String text, final int defaultCode) { + if (text == null) { + return defaultCode; } - ArrayList<String> out = null; - for (int i = 0; i < array.length; i++) { - final String entry = array[i]; - if (TextUtils.isEmpty(entry)) { - if (out == null) { - out = arrayAsList(array, 0, i); - } - } else if (out != null) { - out.add(entry); - } + if (text.startsWith(KeyboardCodesSet.PREFIX_CODE)) { + return KeyboardCodesSet.getCode(text.substring(KeyboardCodesSet.PREFIX_CODE.length())); } - if (out == null) { - return array; + // This is a workaround to have a key that has a supplementary code point. We can't put a + // string in resource as a XML entity of a supplementary code point or a surrogate pair. + if (text.startsWith(PREFIX_HEX)) { + return Integer.parseInt(text.substring(PREFIX_HEX.length()), 16); } - return out.toArray(new String[out.size()]); + return defaultCode; } - public static String[] insertAdditionalMoreKeys(final String[] moreKeySpecs, - final String[] additionalMoreKeySpecs) { - final String[] moreKeys = filterOutEmptyString(moreKeySpecs); - final String[] additionalMoreKeys = filterOutEmptyString(additionalMoreKeySpecs); - final int moreKeysCount = moreKeys.length; - final int additionalCount = additionalMoreKeys.length; - ArrayList<String> out = null; - int additionalIndex = 0; - for (int moreKeyIndex = 0; moreKeyIndex < moreKeysCount; moreKeyIndex++) { - final String moreKeySpec = moreKeys[moreKeyIndex]; - if (moreKeySpec.equals(ADDITIONAL_MORE_KEY_MARKER)) { - if (additionalIndex < additionalCount) { - // Replace '%' marker with additional more key specification. - final String additionalMoreKey = additionalMoreKeys[additionalIndex]; - if (out != null) { - out.add(additionalMoreKey); - } else { - moreKeys[moreKeyIndex] = additionalMoreKey; - } - additionalIndex++; - } else { - // Filter out excessive '%' marker. - if (out == null) { - out = arrayAsList(moreKeys, 0, moreKeyIndex); - } - } - } else { - if (out != null) { - out.add(moreKeySpec); - } - } + public static int getIconId(final String keySpec) { + if (keySpec == null) { + // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory. + return KeyboardIconsSet.ICON_UNDEFINED; } - if (additionalCount > 0 && additionalIndex == 0) { - // No '%' marker is found in more keys. - // Insert all additional more keys to the head of more keys. - if (DEBUG && out != null) { - throw new RuntimeException("Internal logic error:" - + " moreKeys=" + Arrays.toString(moreKeys) - + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys)); - } - out = arrayAsList(additionalMoreKeys, additionalIndex, additionalCount); - for (int i = 0; i < moreKeysCount; i++) { - out.add(moreKeys[i]); - } - } else if (additionalIndex < additionalCount) { - // The number of '%' markers are less than additional more keys. - // Append remained additional more keys to the tail of more keys. - if (DEBUG && out != null) { - throw new RuntimeException("Internal logic error:" - + " moreKeys=" + Arrays.toString(moreKeys) - + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys)); - } - out = arrayAsList(moreKeys, 0, moreKeysCount); - for (int i = additionalIndex; i < additionalCount; i++) { - out.add(additionalMoreKeys[additionalIndex]); - } - } - if (out == null && moreKeysCount > 0) { - return moreKeys; - } else if (out != null && out.size() > 0) { - return out.toArray(new String[out.size()]); - } else { - return null; + if (!hasIcon(keySpec)) { + return KeyboardIconsSet.ICON_UNDEFINED; } + final int labelEnd = indexOfLabelEnd(keySpec); + final String iconName = getBeforeLabelEnd(keySpec, labelEnd) + .substring(KeyboardIconsSet.PREFIX_ICON.length()); + return KeyboardIconsSet.getIconId(iconName); } @SuppressWarnings("serial") @@ -377,122 +246,4 @@ public final class KeySpecParser { super(message); } } - - public static String resolveTextReference(final String rawText, - final KeyboardTextsSet textsSet) { - int level = 0; - String text = rawText; - StringBuilder sb; - do { - level++; - if (level >= MAX_STRING_REFERENCE_INDIRECTION) { - throw new RuntimeException("too many @string/resource indirection: " + text); - } - - final int prefixLen = PREFIX_TEXT.length(); - final int size = text.length(); - if (size < prefixLen) { - return text; - } - - sb = null; - for (int pos = 0; pos < size; pos++) { - final char c = text.charAt(pos); - if (text.startsWith(PREFIX_TEXT, pos) && textsSet != null) { - if (sb == null) { - sb = new StringBuilder(text.substring(0, pos)); - } - final int end = searchTextNameEnd(text, pos + prefixLen); - final String name = text.substring(pos + prefixLen, end); - sb.append(textsSet.getText(name)); - pos = end - 1; - } else if (c == BACKSLASH) { - if (sb != null) { - // Append both escape character and escaped character. - sb.append(text.substring(pos, Math.min(pos + 2, size))); - } - pos++; - } else if (sb != null) { - sb.append(c); - } - } - - if (sb != null) { - text = sb.toString(); - } - } while (sb != null); - return text; - } - - private static int searchTextNameEnd(final String text, final int start) { - final int size = text.length(); - for (int pos = start; pos < size; pos++) { - final char c = text.charAt(pos); - // Label name should be consisted of [a-zA-Z_0-9]. - if ((c >= 'a' && c <= 'z') || c == '_' || (c >= '0' && c <= '9')) { - continue; - } - return pos; - } - return size; - } - - public static int getIntValue(final String[] moreKeys, final String key, - final int defaultValue) { - if (moreKeys == null) { - return defaultValue; - } - final int keyLen = key.length(); - boolean foundValue = false; - int value = defaultValue; - for (int i = 0; i < moreKeys.length; i++) { - final String moreKeySpec = moreKeys[i]; - if (moreKeySpec == null || !moreKeySpec.startsWith(key)) { - continue; - } - moreKeys[i] = null; - try { - if (!foundValue) { - value = Integer.parseInt(moreKeySpec.substring(keyLen)); - foundValue = true; - } - } catch (NumberFormatException e) { - throw new RuntimeException( - "integer should follow after " + key + ": " + moreKeySpec); - } - } - return value; - } - - public static boolean getBooleanValue(final String[] moreKeys, final String key) { - if (moreKeys == null) { - return false; - } - boolean value = false; - for (int i = 0; i < moreKeys.length; i++) { - final String moreKeySpec = moreKeys[i]; - if (moreKeySpec == null || !moreKeySpec.equals(key)) { - continue; - } - moreKeys[i] = null; - value = true; - } - return value; - } - - public static int toUpperCaseOfCodeForLocale(final int code, final boolean needsToUpperCase, - final Locale locale) { - if (!Constants.isLetterCode(code) || !needsToUpperCase) return code; - final String text = new String(new int[] { code } , 0, 1); - final String casedText = KeySpecParser.toUpperCaseOfStringForLocale( - text, needsToUpperCase, locale); - return StringUtils.codePointCount(casedText) == 1 - ? casedText.codePointAt(0) : CODE_UNSPECIFIED; - } - - public static String toUpperCaseOfStringForLocale(final String text, - final boolean needsToUpperCase, final Locale locale) { - if (text == null || !needsToUpperCase) return text; - return text.toUpperCase(locale); - } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java index e6a674334..7941ddd41 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java @@ -32,15 +32,15 @@ public abstract class KeyStyle { protected String parseString(final TypedArray a, final int index) { if (a.hasValue(index)) { - return KeySpecParser.resolveTextReference(a.getString(index), mTextsSet); + return mTextsSet.resolveTextReference(a.getString(index)); } return null; } protected String[] parseStringArray(final TypedArray a, final int index) { if (a.hasValue(index)) { - final String text = KeySpecParser.resolveTextReference(a.getString(index), mTextsSet); - return KeySpecParser.splitKeySpecs(text); + final String text = mTextsSet.resolveTextReference(a.getString(index)); + return MoreKeySpec.splitKeySpecs(text); } return null; } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java index 05d855e31..0b0e761d2 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java @@ -21,19 +21,19 @@ import android.util.Log; import android.util.SparseArray; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.XmlParseUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import java.util.Arrays; import java.util.HashMap; public final class KeyStylesSet { private static final String TAG = KeyStylesSet.class.getSimpleName(); private static final boolean DEBUG = false; - private final HashMap<String, KeyStyle> mStyles = CollectionUtils.newHashMap(); + private final HashMap<String, KeyStyle> mStyles = new HashMap<>(); private final KeyboardTextsSet mTextsSet; private final KeyStyle mEmptyKeyStyle; @@ -74,7 +74,7 @@ public final class KeyStylesSet { private static final class DeclaredKeyStyle extends KeyStyle { private final HashMap<String, KeyStyle> mStyles; private final String mParentStyleName; - private final SparseArray<Object> mStyleAttributes = CollectionUtils.newSparseArray(); + private final SparseArray<Object> mStyleAttributes = new SparseArray<>(); public DeclaredKeyStyle(final String parentStyleName, final KeyboardTextsSet textsSet, final HashMap<String, KeyStyle> styles) { @@ -90,7 +90,8 @@ public final class KeyStylesSet { } final Object value = mStyleAttributes.get(index); if (value != null) { - return (String[])value; + final String[] array = (String[])value; + return Arrays.copyOf(array, array.length); } final KeyStyle parentStyle = mStyles.get(mParentStyleName); return parentStyle.getStringArray(a, index); @@ -133,15 +134,12 @@ public final class KeyStylesSet { public void readKeyAttributes(final TypedArray keyAttr) { // TODO: Currently not all Key attributes can be declared as style. - readString(keyAttr, R.styleable.Keyboard_Key_code); readString(keyAttr, R.styleable.Keyboard_Key_altCode); - readString(keyAttr, R.styleable.Keyboard_Key_keyLabel); - readString(keyAttr, R.styleable.Keyboard_Key_keyOutputText); + readString(keyAttr, R.styleable.Keyboard_Key_keySpec); readString(keyAttr, R.styleable.Keyboard_Key_keyHintLabel); readStringArray(keyAttr, R.styleable.Keyboard_Key_moreKeys); readStringArray(keyAttr, R.styleable.Keyboard_Key_additionalMoreKeys); readFlags(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags); - readString(keyAttr, R.styleable.Keyboard_Key_keyIcon); readString(keyAttr, R.styleable.Keyboard_Key_keyIconDisabled); readString(keyAttr, R.styleable.Keyboard_Key_keyIconPreview); readInt(keyAttr, R.styleable.Keyboard_Key_maxMoreKeysColumn); diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java b/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java index 8bdad364c..133462ac7 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java @@ -16,7 +16,6 @@ package com.android.inputmethod.keyboard.internal; -import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Typeface; import android.util.SparseIntArray; @@ -38,15 +37,18 @@ public final class KeyVisualAttributes { public final float mHintLabelRatio; public final float mPreviewTextRatio; - public final ColorStateList mTextColorStateList; + public final int mTextColor; public final int mTextInactivatedColor; public final int mTextShadowColor; + public final int mFunctionalTextColor; public final int mHintLetterColor; public final int mHintLabelColor; public final int mShiftedLetterHintInactivatedColor; public final int mShiftedLetterHintActivatedColor; public final int mPreviewTextColor; + public final float mHintLabelVerticalAdjustment; + private static final int[] VISUAL_ATTRIBUTE_IDS = { R.styleable.Keyboard_Key_keyTypeface, R.styleable.Keyboard_Key_keyLetterSize, @@ -60,11 +62,13 @@ public final class KeyVisualAttributes { R.styleable.Keyboard_Key_keyTextColor, R.styleable.Keyboard_Key_keyTextInactivatedColor, R.styleable.Keyboard_Key_keyTextShadowColor, + R.styleable.Keyboard_Key_functionalTextColor, 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, + R.styleable.Keyboard_Key_keyHintLabelVerticalAdjustment, }; private static final SparseIntArray sVisualAttributeIds = new SparseIntArray(); private static final int ATTR_DEFINED = 1; @@ -116,10 +120,11 @@ public final class KeyVisualAttributes { mPreviewTextRatio = ResourceUtils.getFraction(keyAttr, R.styleable.Keyboard_Key_keyPreviewTextRatio); - mTextColorStateList = keyAttr.getColorStateList(R.styleable.Keyboard_Key_keyTextColor); + mTextColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyTextColor, 0); mTextInactivatedColor = keyAttr.getColor( R.styleable.Keyboard_Key_keyTextInactivatedColor, 0); mTextShadowColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyTextShadowColor, 0); + mFunctionalTextColor = keyAttr.getColor(R.styleable.Keyboard_Key_functionalTextColor, 0); mHintLetterColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyHintLetterColor, 0); mHintLabelColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyHintLabelColor, 0); mShiftedLetterHintInactivatedColor = keyAttr.getColor( @@ -127,5 +132,8 @@ public final class KeyVisualAttributes { mShiftedLetterHintActivatedColor = keyAttr.getColor( R.styleable.Keyboard_Key_keyShiftedLetterHintActivatedColor, 0); mPreviewTextColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyPreviewTextColor, 0); + + mHintLabelVerticalAdjustment = ResourceUtils.getFraction(keyAttr, + R.styleable.Keyboard_Key_keyHintLabelVerticalAdjustment, 0.0f); } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java index c1ae65695..8bff27574 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java @@ -21,6 +21,7 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.os.Build; +import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; @@ -33,17 +34,16 @@ import com.android.inputmethod.keyboard.KeyboardId; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.utils.ResourceUtils; -import com.android.inputmethod.latin.utils.RunInLocale; import com.android.inputmethod.latin.utils.StringUtils; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; import com.android.inputmethod.latin.utils.XmlParseUtils; +import com.android.inputmethod.latin.utils.XmlParseUtils.ParseException; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.util.Arrays; -import java.util.Locale; /** * Keyboard Building helper. @@ -276,20 +276,7 @@ public class KeyboardBuilder<KP extends KeyboardParams> { params.mThemeId = keyboardAttr.getInt(R.styleable.Keyboard_themeId, 0); params.mIconsSet.loadIcons(keyboardAttr); - final String language = params.mId.mLocale.getLanguage(); - params.mCodesSet.setLanguage(language); - params.mTextsSet.setLanguage(language); - final RunInLocale<Void> job = new RunInLocale<Void>() { - @Override - protected Void job(final Resources res) { - params.mTextsSet.loadStringResources(mContext); - return null; - } - }; - // Null means the current system locale. - final Locale locale = SubtypeLocaleUtils.isNoLanguage(params.mId.mSubtype) - ? null : params.mId.mLocale; - job.runInLocale(mResources, locale); + params.mTextsSet.setLocale(params.mId.mLocale, mContext); final int resourceId = keyboardAttr.getResourceId( R.styleable.Keyboard_touchPositionCorrectionData, 0); @@ -456,11 +443,16 @@ public class KeyboardBuilder<KP extends KeyboardParams> { if (Build.VERSION.SDK_INT < supportedMinSdkVersion) { continue; } + final int labelFlags = row.getDefaultKeyLabelFlags(); + // TODO: Should be able to assign default keyActionFlags as well. + final int backgroundType = row.getDefaultBackgroundType(); final int x = (int)row.getKeyX(null); final int y = row.getKeyY(); - final Key key = new Key(mParams, label, null /* hintLabel */, 0 /* iconId */, - code, outputText, x, y, (int)keyWidth, (int)row.getRowHeight(), - row.getDefaultKeyLabelFlags(), row.getDefaultBackgroundType()); + final int width = (int)keyWidth; + final int height = row.getRowHeight(); + final Key key = new Key(label, KeyboardIconsSet.ICON_UNDEFINED, code, outputText, + null /* hintLabel */, labelFlags, backgroundType, x, y, width, height, + mParams.mHorizontalGap, mParams.mVerticalGap); endKey(key); row.advanceXPos(keyWidth); } @@ -477,7 +469,15 @@ public class KeyboardBuilder<KP extends KeyboardParams> { if (DEBUG) startEndTag("<%s /> skipped", TAG_KEY); return; } - final Key key = new Key(mResources, mParams, row, parser); + final TypedArray keyAttr = mResources.obtainAttributes( + Xml.asAttributeSet(parser), R.styleable.Keyboard_Key); + final KeyStyle keyStyle = mParams.mKeyStyles.getKeyStyle(keyAttr, parser); + final String keySpec = keyStyle.getString(keyAttr, R.styleable.Keyboard_Key_keySpec); + if (TextUtils.isEmpty(keySpec)) { + throw new ParseException("Empty keySpec", parser); + } + final Key key = new Key(keySpec, keyAttr, keyStyle, mParams, row); + keyAttr.recycle(); if (DEBUG) { startEndTag("<%s%s %s moreKeys=%s />", TAG_KEY, (key.isEnabled() ? "" : " disabled"), key, Arrays.toString(key.getMoreKeys())); @@ -493,7 +493,11 @@ public class KeyboardBuilder<KP extends KeyboardParams> { if (DEBUG) startEndTag("<%s /> skipped", TAG_SPACER); return; } - final Key.Spacer spacer = new Key.Spacer(mResources, mParams, row, parser); + final TypedArray keyAttr = mResources.obtainAttributes( + Xml.asAttributeSet(parser), R.styleable.Keyboard_Key); + final KeyStyle keyStyle = mParams.mKeyStyles.getKeyStyle(keyAttr, parser); + final Key spacer = new Key.Spacer(keyAttr, keyStyle, mParams, row); + keyAttr.recycle(); if (DEBUG) startEndTag("<%s />", TAG_SPACER); XmlParseUtils.checkEndTag(TAG_SPACER, parser); endKey(spacer); @@ -649,10 +653,6 @@ public class KeyboardBuilder<KP extends KeyboardParams> { R.styleable.Keyboard_Case_passwordInput, id.passwordInput()); final boolean clobberSettingsKeyMatched = matchBoolean(caseAttr, R.styleable.Keyboard_Case_clobberSettingsKey, id.mClobberSettingsKey); - final boolean shortcutKeyEnabledMatched = matchBoolean(caseAttr, - R.styleable.Keyboard_Case_shortcutKeyEnabled, id.mShortcutKeyEnabled); - final boolean shortcutKeyOnSymbolsMatched = matchBoolean(caseAttr, - R.styleable.Keyboard_Case_shortcutKeyOnSymbols, id.mShortcutKeyOnSymbols); final boolean hasShortcutKeyMatched = matchBoolean(caseAttr, R.styleable.Keyboard_Case_hasShortcutKey, id.mHasShortcutKey); final boolean languageSwitchKeyEnabledMatched = matchBoolean(caseAttr, @@ -662,6 +662,8 @@ public class KeyboardBuilder<KP extends KeyboardParams> { R.styleable.Keyboard_Case_isMultiLine, id.isMultiLine()); final boolean imeActionMatched = matchInteger(caseAttr, R.styleable.Keyboard_Case_imeAction, id.imeAction()); + final boolean isIconDefinedMatched = isIconDefined(caseAttr, + R.styleable.Keyboard_Case_isIconDefined, mParams.mIconsSet); final boolean localeCodeMatched = matchString(caseAttr, R.styleable.Keyboard_Case_localeCode, id.mLocale.toString()); final boolean languageCodeMatched = matchString(caseAttr, @@ -670,14 +672,13 @@ public class KeyboardBuilder<KP extends KeyboardParams> { R.styleable.Keyboard_Case_countryCode, id.mLocale.getCountry()); final boolean selected = keyboardLayoutSetMatched && keyboardLayoutSetElementMatched && modeMatched && navigateNextMatched && navigatePreviousMatched - && passwordInputMatched && clobberSettingsKeyMatched - && shortcutKeyEnabledMatched && shortcutKeyOnSymbolsMatched - && hasShortcutKeyMatched && languageSwitchKeyEnabledMatched - && isMultiLineMatched && imeActionMatched && localeCodeMatched - && languageCodeMatched && countryCodeMatched; + && passwordInputMatched && clobberSettingsKeyMatched && hasShortcutKeyMatched + && languageSwitchKeyEnabledMatched && isMultiLineMatched && imeActionMatched + && isIconDefinedMatched && localeCodeMatched && languageCodeMatched + && countryCodeMatched; if (DEBUG) { - startTag("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s>%s", TAG_CASE, + startTag("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s>%s", TAG_CASE, textAttr(caseAttr.getString( R.styleable.Keyboard_Case_keyboardLayoutSet), "keyboardLayoutSet"), textAttr(caseAttr.getString( @@ -694,16 +695,14 @@ public class KeyboardBuilder<KP extends KeyboardParams> { "clobberSettingsKey"), booleanAttr(caseAttr, R.styleable.Keyboard_Case_passwordInput, "passwordInput"), - booleanAttr(caseAttr, R.styleable.Keyboard_Case_shortcutKeyEnabled, - "shortcutKeyEnabled"), - booleanAttr(caseAttr, R.styleable.Keyboard_Case_shortcutKeyOnSymbols, - "shortcutKeyOnSymbols"), booleanAttr(caseAttr, R.styleable.Keyboard_Case_hasShortcutKey, "hasShortcutKey"), booleanAttr(caseAttr, R.styleable.Keyboard_Case_languageSwitchKeyEnabled, "languageSwitchKeyEnabled"), booleanAttr(caseAttr, R.styleable.Keyboard_Case_isMultiLine, "isMultiLine"), + textAttr(caseAttr.getString(R.styleable.Keyboard_Case_isIconDefined), + "isIconDefined"), textAttr(caseAttr.getString(R.styleable.Keyboard_Case_localeCode), "localeCode"), textAttr(caseAttr.getString(R.styleable.Keyboard_Case_languageCode), @@ -755,6 +754,16 @@ public class KeyboardBuilder<KP extends KeyboardParams> { return false; } + private static boolean isIconDefined(final TypedArray a, final int index, + final KeyboardIconsSet iconsSet) { + if (!a.hasValue(index)) { + return true; + } + final String iconName = a.getString(index); + final int iconId = KeyboardIconsSet.getIconId(iconName); + return iconsSet.getIconDrawable(iconId) != null; + } + private boolean parseDefault(final XmlPullParser parser, final KeyboardRow row, final boolean skip) throws XmlPullParserException, IOException { if (DEBUG) startTag("<%s>", TAG_DEFAULT); diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java index dc815e57d..62b69dcc9 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java @@ -17,25 +17,22 @@ package com.android.inputmethod.keyboard.internal; import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.utils.CollectionUtils; import java.util.HashMap; public final class KeyboardCodesSet { - private static final HashMap<String, int[]> sLanguageToCodesMap = CollectionUtils.newHashMap(); - private static final HashMap<String, Integer> sNameToIdMap = CollectionUtils.newHashMap(); + public static final String PREFIX_CODE = "!code/"; - private int[] mCodes = DEFAULT; + private static final HashMap<String, Integer> sNameToIdMap = new HashMap<>(); - public void setLanguage(final String language) { - final int[] codes = sLanguageToCodesMap.get(language); - mCodes = (codes != null) ? codes : DEFAULT; + private KeyboardCodesSet() { + // This utility class is not publicly instantiable. } - public int getCode(final String name) { + public static int getCode(final String name) { Integer id = sNameToIdMap.get(name); if (id == null) throw new RuntimeException("Unknown key code: " + name); - return mCodes[id]; + return DEFAULT[id]; } private static final String[] ID_TO_NAME = { @@ -54,27 +51,10 @@ public final class KeyboardCodesSet { "key_shift_enter", "key_language_switch", "key_emoji", + "key_alpha_from_emoji", "key_unspecified", - "key_left_parenthesis", - "key_right_parenthesis", - "key_less_than", - "key_greater_than", - "key_left_square_bracket", - "key_right_square_bracket", - "key_left_curly_bracket", - "key_right_curly_bracket", }; - private static final int CODE_LEFT_PARENTHESIS = '('; - private static final int CODE_RIGHT_PARENTHESIS = ')'; - private static final int CODE_LESS_THAN_SIGN = '<'; - private static final int CODE_GREATER_THAN_SIGN = '>'; - private static final int CODE_LEFT_SQUARE_BRACKET = '['; - private static final int CODE_RIGHT_SQUARE_BRACKET = ']'; - private static final int CODE_LEFT_CURLY_BRACKET = '{'; - private static final int CODE_RIGHT_CURLY_BRACKET = '}'; - - // This array should be aligned with the array RTL below. private static final int[] DEFAULT = { Constants.CODE_TAB, Constants.CODE_ENTER, @@ -91,68 +71,13 @@ public final class KeyboardCodesSet { Constants.CODE_SHIFT_ENTER, Constants.CODE_LANGUAGE_SWITCH, Constants.CODE_EMOJI, + Constants.CODE_ALPHA_FROM_EMOJI, Constants.CODE_UNSPECIFIED, - CODE_LEFT_PARENTHESIS, - CODE_RIGHT_PARENTHESIS, - CODE_LESS_THAN_SIGN, - CODE_GREATER_THAN_SIGN, - CODE_LEFT_SQUARE_BRACKET, - CODE_RIGHT_SQUARE_BRACKET, - CODE_LEFT_CURLY_BRACKET, - CODE_RIGHT_CURLY_BRACKET, - }; - - private static final int[] RTL = { - DEFAULT[0], - DEFAULT[1], - DEFAULT[2], - DEFAULT[3], - DEFAULT[4], - DEFAULT[5], - DEFAULT[6], - DEFAULT[7], - DEFAULT[8], - DEFAULT[9], - DEFAULT[10], - DEFAULT[11], - DEFAULT[12], - DEFAULT[13], - DEFAULT[14], - DEFAULT[15], - CODE_RIGHT_PARENTHESIS, - CODE_LEFT_PARENTHESIS, - CODE_GREATER_THAN_SIGN, - CODE_LESS_THAN_SIGN, - CODE_RIGHT_SQUARE_BRACKET, - CODE_LEFT_SQUARE_BRACKET, - CODE_RIGHT_CURLY_BRACKET, - CODE_LEFT_CURLY_BRACKET, - }; - - private static final String LANGUAGE_DEFAULT = "DEFAULT"; - private static final String LANGUAGE_ARABIC = "ar"; - private static final String LANGUAGE_PERSIAN = "fa"; - private static final String LANGUAGE_HEBREW = "iw"; - - private static final Object[] LANGUAGE_AND_CODES = { - LANGUAGE_DEFAULT, DEFAULT, - LANGUAGE_ARABIC, RTL, - LANGUAGE_PERSIAN, RTL, - LANGUAGE_HEBREW, RTL, }; static { - if (DEFAULT.length != RTL.length || DEFAULT.length != ID_TO_NAME.length) { - throw new RuntimeException("Internal inconsistency"); - } for (int i = 0; i < ID_TO_NAME.length; i++) { sNameToIdMap.put(ID_TO_NAME[i], i); } - - for (int i = 0; i < LANGUAGE_AND_CODES.length; i += 2) { - final String language = (String)LANGUAGE_AND_CODES[i]; - final int[] codes = (int[])LANGUAGE_AND_CODES[i + 1]; - sLanguageToCodesMap.put(language, codes); - } } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java index 336db186e..7146deb4b 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java @@ -23,45 +23,73 @@ import android.util.Log; import android.util.SparseIntArray; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.utils.CollectionUtils; import java.util.HashMap; public final class KeyboardIconsSet { private static final String TAG = KeyboardIconsSet.class.getSimpleName(); + public static final String PREFIX_ICON = "!icon/"; public static final int ICON_UNDEFINED = 0; private static final int ATTR_UNDEFINED = 0; + private static final String NAME_UNDEFINED = "undefined"; + public static final String NAME_SHIFT_KEY = "shift_key"; + public static final String NAME_SHIFT_KEY_SHIFTED = "shift_key_shifted"; + public static final String NAME_DELETE_KEY = "delete_key"; + public static final String NAME_SETTINGS_KEY = "settings_key"; + public static final String NAME_SPACE_KEY = "space_key"; + public static final String NAME_SPACE_KEY_FOR_NUMBER_LAYOUT = "space_key_for_number_layout"; + public static final String NAME_ENTER_KEY = "enter_key"; + public static final String NAME_GO_KEY = "go_key"; + public static final String NAME_SEARCH_KEY = "search_key"; + public static final String NAME_SEND_KEY = "send_key"; + public static final String NAME_NEXT_KEY = "next_key"; + public static final String NAME_DONE_KEY = "done_key"; + public static final String NAME_PREVIOUS_KEY = "previous_key"; + public static final String NAME_TAB_KEY = "tab_key"; + public static final String NANE_TAB_KEY_PREVIEW = "tab_key_preview"; + public static final String NAME_SHORTCUT_KEY = "shortcut_key"; + public static final String NAME_SHORTCUT_KEY_DISABLED = "shortcut_key_disabled"; + public static final String NAME_LANGUAGE_SWITCH_KEY = "language_switch_key"; + public static final String NAME_ZWNJ_KEY = "zwnj_key"; + public static final String NAME_ZWJ_KEY = "zwj_key"; + public static final String NAME_EMOJI_KEY = "emoji_key"; + private static final SparseIntArray ATTR_ID_TO_ICON_ID = new SparseIntArray(); // Icon name to icon id map. - private static final HashMap<String, Integer> sNameToIdsMap = CollectionUtils.newHashMap(); + private static final HashMap<String, Integer> sNameToIdsMap = new HashMap<>(); private static final Object[] NAMES_AND_ATTR_IDS = { - "undefined", ATTR_UNDEFINED, - "shift_key", R.styleable.Keyboard_iconShiftKey, - "delete_key", R.styleable.Keyboard_iconDeleteKey, - "settings_key", R.styleable.Keyboard_iconSettingsKey, - "space_key", R.styleable.Keyboard_iconSpaceKey, - "enter_key", R.styleable.Keyboard_iconEnterKey, - "search_key", R.styleable.Keyboard_iconSearchKey, - "tab_key", R.styleable.Keyboard_iconTabKey, - "shortcut_key", R.styleable.Keyboard_iconShortcutKey, - "shortcut_for_label", R.styleable.Keyboard_iconShortcutForLabel, - "space_key_for_number_layout", R.styleable.Keyboard_iconSpaceKeyForNumberLayout, - "shift_key_shifted", R.styleable.Keyboard_iconShiftKeyShifted, - "shortcut_key_disabled", R.styleable.Keyboard_iconShortcutKeyDisabled, - "tab_key_preview", R.styleable.Keyboard_iconTabKeyPreview, - "language_switch_key", R.styleable.Keyboard_iconLanguageSwitchKey, - "zwnj_key", R.styleable.Keyboard_iconZwnjKey, - "zwj_key", R.styleable.Keyboard_iconZwjKey, - "emoji_key", R.styleable.Keyboard_iconEmojiKey, + NAME_UNDEFINED, ATTR_UNDEFINED, + NAME_SHIFT_KEY, R.styleable.Keyboard_iconShiftKey, + NAME_DELETE_KEY, R.styleable.Keyboard_iconDeleteKey, + NAME_SETTINGS_KEY, R.styleable.Keyboard_iconSettingsKey, + NAME_SPACE_KEY, R.styleable.Keyboard_iconSpaceKey, + NAME_ENTER_KEY, R.styleable.Keyboard_iconEnterKey, + NAME_GO_KEY, R.styleable.Keyboard_iconGoKey, + NAME_SEARCH_KEY, R.styleable.Keyboard_iconSearchKey, + NAME_SEND_KEY, R.styleable.Keyboard_iconSendKey, + NAME_NEXT_KEY, R.styleable.Keyboard_iconNextKey, + NAME_DONE_KEY, R.styleable.Keyboard_iconDoneKey, + NAME_PREVIOUS_KEY, R.styleable.Keyboard_iconPreviousKey, + NAME_TAB_KEY, R.styleable.Keyboard_iconTabKey, + NAME_SHORTCUT_KEY, R.styleable.Keyboard_iconShortcutKey, + NAME_SPACE_KEY_FOR_NUMBER_LAYOUT, R.styleable.Keyboard_iconSpaceKeyForNumberLayout, + NAME_SHIFT_KEY_SHIFTED, R.styleable.Keyboard_iconShiftKeyShifted, + NAME_SHORTCUT_KEY_DISABLED, R.styleable.Keyboard_iconShortcutKeyDisabled, + NANE_TAB_KEY_PREVIEW, R.styleable.Keyboard_iconTabKeyPreview, + NAME_LANGUAGE_SWITCH_KEY, R.styleable.Keyboard_iconLanguageSwitchKey, + NAME_ZWNJ_KEY, R.styleable.Keyboard_iconZwnjKey, + NAME_ZWJ_KEY, R.styleable.Keyboard_iconZwjKey, + NAME_EMOJI_KEY, R.styleable.Keyboard_iconEmojiKey, }; private static int NUM_ICONS = NAMES_AND_ATTR_IDS.length / 2; private static final String[] ICON_NAMES = new String[NUM_ICONS]; private final Drawable[] mIcons = new Drawable[NUM_ICONS]; + private final int[] mIconResourceIds = new int[NUM_ICONS]; static { int iconId = ICON_UNDEFINED; @@ -69,7 +97,7 @@ public final class KeyboardIconsSet { final String name = (String)NAMES_AND_ATTR_IDS[i]; final Integer attrId = (Integer)NAMES_AND_ATTR_IDS[i + 1]; if (attrId != ATTR_UNDEFINED) { - ATTR_ID_TO_ICON_ID.put(attrId, iconId); + ATTR_ID_TO_ICON_ID.put(attrId, iconId); } sNameToIdsMap.put(name, iconId); ICON_NAMES[iconId] = name; @@ -86,6 +114,7 @@ public final class KeyboardIconsSet { setDefaultBounds(icon); final Integer iconId = ATTR_ID_TO_ICON_ID.get(attrId); mIcons[iconId] = icon; + mIconResourceIds[iconId] = keyboardAttrs.getResourceId(attrId, 0); } catch (Resources.NotFoundException e) { Log.w(TAG, "Drawable resource for icon #" + keyboardAttrs.getResources().getResourceEntryName(attrId) @@ -102,7 +131,7 @@ public final class KeyboardIconsSet { return isValidIconId(iconId) ? ICON_NAMES[iconId] : "unknown<" + iconId + ">"; } - static int getIconId(final String name) { + public static int getIconId(final String name) { Integer iconId = sNameToIdsMap.get(name); if (iconId != null) { return iconId; @@ -110,6 +139,14 @@ public final class KeyboardIconsSet { throw new RuntimeException("unknown icon name: " + name); } + public int getIconResourceId(final String name) { + final int iconId = getIconId(name); + if (isValidIconId(iconId)) { + return mIconResourceIds[iconId]; + } + throw new RuntimeException("unknown icon name: " + name); + } + public Drawable getIconDrawable(final int iconId) { if (isValidIconId(iconId)) { return mIcons[iconId]; diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java index d32bb7581..5df9d3ece 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java @@ -21,9 +21,10 @@ import android.util.SparseIntArray; import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.KeyboardId; import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.utils.CollectionUtils; import java.util.ArrayList; +import java.util.Comparator; +import java.util.SortedSet; import java.util.TreeSet; public class KeyboardParams { @@ -58,11 +59,11 @@ public class KeyboardParams { 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(); + // Keys are sorted from top-left to bottom-right order. + public final SortedSet<Key> mSortedKeys = new TreeSet<>(ROW_COLUMN_COMPARATOR); + public final ArrayList<Key> mShiftKeys = new ArrayList<>(); + public final ArrayList<Key> mAltCodeKeysWhileTyping = new ArrayList<>(); 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); @@ -76,8 +77,20 @@ public class KeyboardParams { public final TouchPositionCorrection mTouchPositionCorrection = new TouchPositionCorrection(); + // Comparator to sort {@link Key}s from top-left to bottom-right order. + private static final Comparator<Key> ROW_COLUMN_COMPARATOR = new Comparator<Key>() { + @Override + public int compare(final Key lhs, final Key rhs) { + if (lhs.getY() < rhs.getY()) return -1; + if (lhs.getY() > rhs.getY()) return 1; + if (lhs.getX() < rhs.getX()) return -1; + if (lhs.getX() > rhs.getX()) return 1; + return 0; + } + }; + protected void clearKeys() { - mKeys.clear(); + mSortedKeys.clear(); mShiftKeys.clear(); clearHistogram(); } @@ -89,7 +102,7 @@ public class KeyboardParams { // Ignore zero width {@link Spacer}. return; } - mKeys.add(key); + mSortedKeys.add(key); if (isSpacer) { return; } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java index 0f9497c27..92daf0742 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java @@ -23,7 +23,6 @@ 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.utils.CollectionUtils; import com.android.inputmethod.latin.utils.ResourceUtils; import org.xmlpull.v1.XmlPullParser; @@ -44,8 +43,9 @@ public final class KeyboardRow { /** The height of this row. */ private final int mRowHeight; - private final ArrayDeque<RowAttributes> mRowAttributesStack = CollectionUtils.newArrayDeque(); + private final ArrayDeque<RowAttributes> mRowAttributesStack = new ArrayDeque<>(); + // TODO: Add keyActionFlags. private static class RowAttributes { /** Default width of a key in this row. */ public final float mDefaultKeyWidth; diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java index dd98c1703..b98ced97c 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java @@ -27,10 +27,10 @@ import com.android.inputmethod.latin.utils.RecapitalizeStatus; * * This class contains all keyboard state transition logic. * - * The input events are {@link #onLoadKeyboard()}, {@link #onSaveKeyboardState()}, - * {@link #onPressKey(int,boolean,int)}, {@link #onReleaseKey(int,boolean)}, - * {@link #onCodeInput(int,int)}, {@link #onFinishSlidingInput()}, - * {@link #onUpdateShiftState(int,int)}, {@link #onResetKeyboardStateToAlphabet()}. + * The input events are {@link #onLoadKeyboard(int, int)}, {@link #onSaveKeyboardState()}, + * {@link #onPressKey(int,boolean,int,int)}, {@link #onReleaseKey(int,boolean,int,int)}, + * {@link #onCodeInput(int,int,int)}, {@link #onFinishSlidingInput(int,int)}, + * {@link #onUpdateShiftState(int,int)}, {@link #onResetKeyboardStateToAlphabet(int,int)}. * * The actions are {@link SwitchActions}'s methods. */ @@ -52,7 +52,8 @@ public final class KeyboardState { /** * Request to call back {@link KeyboardState#onUpdateShiftState(int, int)}. */ - public void requestUpdatingShiftState(); + public void requestUpdatingShiftState(final int currentAutoCapsState, + final int currentRecapitalizeState); public void startDoubleTapShiftKeyTimer(); public boolean isInDoubleTapShiftKeyTimeout(); @@ -117,7 +118,8 @@ public final class KeyboardState { mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE; } - public void onLoadKeyboard() { + public void onLoadKeyboard(final int currentAutoCapsState, + final int currentRecapitalizeState) { if (DEBUG_EVENT) { Log.d(TAG, "onLoadKeyboard: " + this); } @@ -127,7 +129,7 @@ public final class KeyboardState { mPrevSymbolsKeyboardWasShifted = false; mShiftKeyState.onRelease(); mSymbolKeyState.onRelease(); - onRestoreKeyboardState(); + onRestoreKeyboardState(currentAutoCapsState, currentRecapitalizeState); } private static final int UNSHIFT = 0; @@ -153,13 +155,14 @@ public final class KeyboardState { } } - private void onRestoreKeyboardState() { + private void onRestoreKeyboardState(final int currentAutoCapsState, + final int currentRecapitalizeState) { final SavedKeyboardState state = mSavedKeyboardState; if (DEBUG_EVENT) { Log.d(TAG, "onRestoreKeyboardState: saved=" + state + " " + this); } if (!state.mIsValid || state.mIsAlphabetMode) { - setAlphabetKeyboard(); + setAlphabetKeyboard(currentAutoCapsState, currentRecapitalizeState); } else if (state.mIsEmojiMode) { setEmojiKeyboard(); } else { @@ -237,7 +240,8 @@ public final class KeyboardState { mAlphabetShiftState.setShiftLocked(shiftLocked); } - private void toggleAlphabetAndSymbols() { + private void toggleAlphabetAndSymbols(final int currentAutoCapsState, + final int currentRecapitalizeState) { if (DEBUG_ACTION) { Log.d(TAG, "toggleAlphabetAndSymbols: " + this); } @@ -251,7 +255,7 @@ public final class KeyboardState { mPrevSymbolsKeyboardWasShifted = false; } else { mPrevSymbolsKeyboardWasShifted = mIsSymbolShifted; - setAlphabetKeyboard(); + setAlphabetKeyboard(currentAutoCapsState, currentRecapitalizeState); if (mPrevMainKeyboardWasShiftLocked) { setShiftLocked(true); } @@ -261,14 +265,15 @@ public final class KeyboardState { // TODO: Remove this method. Come up with a more comprehensive way to reset the keyboard layout // when a keyboard layout set doesn't get reloaded in LatinIME.onStartInputViewInternal(). - private void resetKeyboardStateToAlphabet() { + private void resetKeyboardStateToAlphabet(final int currentAutoCapsState, + final int currentRecapitalizeState) { if (DEBUG_ACTION) { Log.d(TAG, "resetKeyboardStateToAlphabet: " + this); } if (mIsAlphabetMode) return; mPrevSymbolsKeyboardWasShifted = mIsSymbolShifted; - setAlphabetKeyboard(); + setAlphabetKeyboard(currentAutoCapsState, currentRecapitalizeState); if (mPrevMainKeyboardWasShiftLocked) { setShiftLocked(true); } @@ -283,7 +288,8 @@ public final class KeyboardState { } } - private void setAlphabetKeyboard() { + private void setAlphabetKeyboard(final int currentAutoCapsState, + final int currentRecapitalizeState) { if (DEBUG_ACTION) { Log.d(TAG, "setAlphabetKeyboard"); } @@ -294,7 +300,7 @@ public final class KeyboardState { mIsSymbolShifted = false; mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE; mSwitchState = SWITCH_STATE_ALPHA; - mSwitchActions.requestUpdatingShiftState(); + mSwitchActions.requestUpdatingShiftState(currentAutoCapsState, currentRecapitalizeState); } private void setSymbolsKeyboard() { @@ -304,6 +310,7 @@ public final class KeyboardState { mSwitchActions.setSymbolsKeyboard(); mIsAlphabetMode = false; mIsSymbolShifted = false; + mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE; // Reset alphabet shift state. mAlphabetShiftState.setShiftLocked(false); mSwitchState = SWITCH_STATE_SYMBOL_BEGIN; @@ -316,6 +323,7 @@ public final class KeyboardState { mSwitchActions.setSymbolsShiftedKeyboard(); mIsAlphabetMode = false; mIsSymbolShifted = true; + mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE; // Reset alphabet shift state. mAlphabetShiftState.setShiftLocked(false); mSwitchState = SWITCH_STATE_SYMBOL_BEGIN; @@ -327,16 +335,18 @@ public final class KeyboardState { } mIsAlphabetMode = false; mIsEmojiMode = true; + mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE; // Remember caps lock mode and reset alphabet shift state. mPrevMainKeyboardWasShiftLocked = mAlphabetShiftState.isShiftLocked(); mAlphabetShiftState.setShiftLocked(false); mSwitchActions.setEmojiKeyboard(); } - public void onPressKey(final int code, final boolean isSinglePointer, final int autoCaps) { + public void onPressKey(final int code, final boolean isSinglePointer, + final int currentAutoCapsState, final int currentRecapitalizeState) { if (DEBUG_EVENT) { - Log.d(TAG, "onPressKey: code=" + Constants.printableCode(code) - + " single=" + isSinglePointer + " autoCaps=" + autoCaps + " " + this); + Log.d(TAG, "onPressKey: code=" + Constants.printableCode(code) + " single=" + + isSinglePointer + " autoCaps=" + currentAutoCapsState + " " + this); } if (code != Constants.CODE_SHIFT) { // Because the double tap shift key timer is to detect two consecutive shift key press, @@ -348,7 +358,7 @@ public final class KeyboardState { } else if (code == Constants.CODE_CAPSLOCK) { // Nothing to do here. See {@link #onReleaseKey(int,boolean)}. } else if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) { - onPressSymbol(); + onPressSymbol(currentAutoCapsState, currentRecapitalizeState); } else { mShiftKeyState.onOtherKeyPressed(); mSymbolKeyState.onOtherKeyPressed(); @@ -360,7 +370,8 @@ public final class KeyboardState { // As for #3, please note that it's required to check even when the auto caps mode is // off because, for example, we may be in the #1 state within the manual temporary // shifted mode. - if (!isSinglePointer && mIsAlphabetMode && autoCaps != TextUtils.CAP_MODE_CHARACTERS) { + if (!isSinglePointer && mIsAlphabetMode + && currentAutoCapsState != TextUtils.CAP_MODE_CHARACTERS) { final boolean needsToResetAutoCaps = mAlphabetShiftState.isAutomaticShifted() || (mAlphabetShiftState.isManualShifted() && mShiftKeyState.isReleasing()); if (needsToResetAutoCaps) { @@ -370,31 +381,34 @@ public final class KeyboardState { } } - public void onReleaseKey(final int code, final boolean withSliding) { + public void onReleaseKey(final int code, final boolean withSliding, + final int currentAutoCapsState, final int currentRecapitalizeState) { if (DEBUG_EVENT) { Log.d(TAG, "onReleaseKey: code=" + Constants.printableCode(code) + " sliding=" + withSliding + " " + this); } if (code == Constants.CODE_SHIFT) { - onReleaseShift(withSliding); + onReleaseShift(withSliding, currentAutoCapsState, currentRecapitalizeState); } else if (code == Constants.CODE_CAPSLOCK) { setShiftLocked(!mAlphabetShiftState.isShiftLocked()); } else if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) { - onReleaseSymbol(withSliding); + onReleaseSymbol(withSliding, currentAutoCapsState, currentRecapitalizeState); } } - private void onPressSymbol() { - toggleAlphabetAndSymbols(); + private void onPressSymbol(final int currentAutoCapsState, + final int currentRecapitalizeState) { + toggleAlphabetAndSymbols(currentAutoCapsState, currentRecapitalizeState); mSymbolKeyState.onPress(); mSwitchState = SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL; } - private void onReleaseSymbol(final boolean withSliding) { + private void onReleaseSymbol(final boolean withSliding, final int currentAutoCapsState, + final int currentRecapitalizeState) { if (mSymbolKeyState.isChording()) { // Switch back to the previous keyboard mode if the user chords the mode change key and // another key, then releases the mode change key. - toggleAlphabetAndSymbols(); + toggleAlphabetAndSymbols(currentAutoCapsState, currentRecapitalizeState); } else if (!withSliding) { // If the mode change key is being released without sliding, we should forget the // previous symbols keyboard shift state and simply switch back to symbols layout @@ -415,11 +429,12 @@ public final class KeyboardState { // TODO: Remove this method. Come up with a more comprehensive way to reset the keyboard layout // when a keyboard layout set doesn't get reloaded in LatinIME.onStartInputViewInternal(). - public void onResetKeyboardStateToAlphabet() { + public void onResetKeyboardStateToAlphabet(final int currentAutoCapsState, + final int currentRecapitalizeState) { if (DEBUG_EVENT) { Log.d(TAG, "onResetKeyboardStateToAlphabet: " + this); } - resetKeyboardStateToAlphabet(); + resetKeyboardStateToAlphabet(currentAutoCapsState, currentRecapitalizeState); } private void updateShiftStateForRecapitalize(final int recapitalizeMode) { @@ -510,7 +525,8 @@ public final class KeyboardState { } } - private void onReleaseShift(final boolean withSliding) { + private void onReleaseShift(final boolean withSliding, final int currentAutoCapsState, + final int currentRecapitalizeState) { if (RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE != mRecapitalizeMode) { // We are recapitalizing. We should match the keyboard state to the recapitalize // state in priority. @@ -533,7 +549,8 @@ public final class KeyboardState { // After chording input, automatic shift state may have been changed depending on // what characters were input. mShiftKeyState.onRelease(); - mSwitchActions.requestUpdatingShiftState(); + mSwitchActions.requestUpdatingShiftState(currentAutoCapsState, + currentRecapitalizeState); return; } else if (mAlphabetShiftState.isShiftLockShifted() && withSliding) { // In shift locked state, shift has been pressed and slid out to other key. @@ -570,20 +587,21 @@ public final class KeyboardState { mShiftKeyState.onRelease(); } - public void onFinishSlidingInput() { + public void onFinishSlidingInput(final int currentAutoCapsState, + final int currentRecapitalizeState) { if (DEBUG_EVENT) { Log.d(TAG, "onFinishSlidingInput: " + this); } // Switch back to the previous keyboard mode if the user cancels sliding input. switch (mSwitchState) { case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL: - toggleAlphabetAndSymbols(); + toggleAlphabetAndSymbols(currentAutoCapsState, currentRecapitalizeState); break; case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE: toggleShiftInSymbols(); break; case SWITCH_STATE_MOMENTARY_ALPHA_SHIFT: - setAlphabetKeyboard(); + setAlphabetKeyboard(currentAutoCapsState, currentRecapitalizeState); break; } } @@ -592,10 +610,11 @@ public final class KeyboardState { return c == Constants.CODE_SPACE || c == Constants.CODE_ENTER; } - public void onCodeInput(final int code, final int autoCaps) { + public void onCodeInput(final int code, final int currentAutoCapsState, + final int currentRecapitalizeState) { if (DEBUG_EVENT) { Log.d(TAG, "onCodeInput: code=" + Constants.printableCode(code) - + " autoCaps=" + autoCaps + " " + this); + + " autoCaps=" + currentAutoCapsState + " " + this); } switch (mSwitchState) { @@ -631,7 +650,7 @@ public final class KeyboardState { // Switch back to alpha keyboard mode if user types one or more non-space/enter // characters followed by a space/enter. if (isSpaceOrEnter(code)) { - toggleAlphabetAndSymbols(); + toggleAlphabetAndSymbols(currentAutoCapsState, currentRecapitalizeState); mPrevSymbolsKeyboardWasShifted = false; } break; @@ -639,9 +658,11 @@ public final class KeyboardState { // If the code is a letter, update keyboard shift state. if (Constants.isLetterCode(code)) { - updateAlphabetShiftState(autoCaps, RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE); + updateAlphabetShiftState(currentAutoCapsState, currentRecapitalizeState); } else if (code == Constants.CODE_EMOJI) { setEmojiKeyboard(); + } else if (code == Constants.CODE_ALPHA_FROM_EMOJI) { + setAlphabetKeyboard(currentAutoCapsState, currentRecapitalizeState); } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java index c2a01b5e8..cd6abeed3 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java @@ -18,3504 +18,132 @@ package com.android.inputmethod.keyboard.internal; import android.content.Context; import android.content.res.Resources; +import android.text.TextUtils; import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.utils.RunInLocale; +import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; import java.util.HashMap; +import java.util.Locale; -/** - * !!!!! DO NOT EDIT THIS FILE !!!!! - * - * This file is generated by tools/make-keyboard-text. The base template file is - * tools/make-keyboard-text/res/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.tmpl - * - * This file must be updated when any text resources in keyboard layout files have been changed. - * These text resources are referred as "!text/<resource_name>" in keyboard XML definitions, - * and should be defined in - * tools/make-keyboard-text/res/values-<locale>/donottranslate-more-keys.xml - * - * To update this file, please run the following commands. - * $ cd $ANDROID_BUILD_TOP - * $ mmm packages/inputmethods/LatinIME/tools/make-keyboard-text - * $ make-keyboard-text -java packages/inputmethods/LatinIME/java/src - * - * The updated source file will be generated to the following path (this file). - * packages/inputmethods/LatinIME/java/src/com/android/inputmethod/keyboard/internal/ - * KeyboardTextsSet.java - */ public final class KeyboardTextsSet { - // Language to texts map. - private static final HashMap<String, String[]> sLocaleToTextsMap = CollectionUtils.newHashMap(); - private static final HashMap<String, Integer> sNameToIdsMap = CollectionUtils.newHashMap(); + public static final String PREFIX_TEXT = "!text/"; + public static final String SWITCH_TO_ALPHA_KEY_LABEL = "keylabel_to_alpha"; - private String[] mTexts; - // Resource name to text map. - private HashMap<String, String> mResourceNameToTextsMap = CollectionUtils.newHashMap(); + private static final char BACKSLASH = Constants.CODE_BACKSLASH; + private static final int MAX_STRING_REFERENCE_INDIRECTION = 10; - public void setLanguage(final String language) { - mTexts = sLocaleToTextsMap.get(language); - if (mTexts == null) { - mTexts = LANGUAGE_DEFAULT; - } - } + private String[] mTextsTable; + // Resource name to text map. + private HashMap<String, String> mResourceNameToTextsMap = new HashMap<>(); - public void loadStringResources(final Context context) { + public void setLocale(final Locale locale, final Context context) { + mTextsTable = KeyboardTextsTable.getTextsTable(locale); + final Resources res = context.getResources(); final int referenceId = context.getApplicationInfo().labelRes; - loadStringResourcesInternal(context, RESOURCE_NAMES, referenceId); + final String resourcePackageName = res.getResourcePackageName(referenceId); + final RunInLocale<Void> job = new RunInLocale<Void>() { + @Override + protected Void job(final Resources resource) { + loadStringResourcesInternal(res, RESOURCE_NAMES, resourcePackageName); + return null; + } + }; + // Null means the current system locale. + job.runInLocale(res, + SubtypeLocaleUtils.NO_LANGUAGE.equals(locale.toString()) ? null : locale); } @UsedForTesting - void loadStringResourcesInternal(final Context context, final String[] resourceNames, - final int referenceId) { - final Resources res = context.getResources(); - final String packageName = res.getResourcePackageName(referenceId); + void loadStringResourcesInternal(final Resources res, final String[] resourceNames, + final String resourcePackageName) { for (final String resName : resourceNames) { - final int resId = res.getIdentifier(resName, "string", packageName); + final int resId = res.getIdentifier(resName, "string", resourcePackageName); mResourceNameToTextsMap.put(resName, res.getString(resId)); } } public String getText(final String name) { - String text = mResourceNameToTextsMap.get(name); - if (text != null) { - return text; + final String text = mResourceNameToTextsMap.get(name); + return (text != null) ? text : KeyboardTextsTable.getText(name, mTextsTable); + } + + private static int searchTextNameEnd(final String text, final int start) { + final int size = text.length(); + for (int pos = start; pos < size; pos++) { + final char c = text.charAt(pos); + // Label name should be consisted of [a-zA-Z_0-9]. + if ((c >= 'a' && c <= 'z') || c == '_' || (c >= '0' && c <= '9')) { + continue; + } + return pos; } - final Integer id = sNameToIdsMap.get(name); - if (id == null) throw new RuntimeException("Unknown label: " + name); - text = (id < mTexts.length) ? mTexts[id] : null; - return (text == null) ? LANGUAGE_DEFAULT[id] : text; + return size; } - private static final String[] RESOURCE_NAMES = { - // These texts' name should be aligned with the @string/<name> in values/strings.xml. + // TODO: Resolve text reference when creating {@link KeyboardTextsTable} class. + public String resolveTextReference(final String rawText) { + if (TextUtils.isEmpty(rawText)) { + return null; + } + int level = 0; + String text = rawText; + StringBuilder sb; + do { + level++; + if (level >= MAX_STRING_REFERENCE_INDIRECTION) { + throw new RuntimeException("Too many " + PREFIX_TEXT + "name indirection: " + text); + } + + final int prefixLen = PREFIX_TEXT.length(); + final int size = text.length(); + if (size < prefixLen) { + break; + } + + sb = null; + for (int pos = 0; pos < size; pos++) { + final char c = text.charAt(pos); + if (text.startsWith(PREFIX_TEXT, pos)) { + if (sb == null) { + sb = new StringBuilder(text.substring(0, pos)); + } + final int end = searchTextNameEnd(text, pos + prefixLen); + final String name = text.substring(pos + prefixLen, end); + sb.append(getText(name)); + pos = end - 1; + } else if (c == BACKSLASH) { + if (sb != null) { + // Append both escape character and escaped character. + sb.append(text.substring(pos, Math.min(pos + 2, size))); + } + pos++; + } else if (sb != null) { + sb.append(c); + } + } + + if (sb != null) { + text = sb.toString(); + } + } while (sb != null); + return TextUtils.isEmpty(text) ? null : text; + } + + // These texts' name should be aligned with the @string/<name> in + // values*/strings-action-keys.xml. + static final String[] RESOURCE_NAMES = { // Labels for action. "label_go_key", - // "label_search_key", "label_send_key", "label_next_key", "label_done_key", + "label_search_key", "label_previous_key", // Other labels. "label_pause_key", "label_wait_key", }; - - private static final String[] NAMES = { - /* 0 */ "more_keys_for_a", - /* 1 */ "more_keys_for_e", - /* 2 */ "more_keys_for_i", - /* 3 */ "more_keys_for_o", - /* 4 */ "more_keys_for_u", - /* 5 */ "more_keys_for_s", - /* 6 */ "more_keys_for_n", - /* 7 */ "more_keys_for_c", - /* 8 */ "more_keys_for_y", - /* 9 */ "more_keys_for_d", - /* 10 */ "more_keys_for_r", - /* 11 */ "more_keys_for_t", - /* 12 */ "more_keys_for_z", - /* 13 */ "more_keys_for_k", - /* 14 */ "more_keys_for_l", - /* 15 */ "more_keys_for_g", - /* 16 */ "more_keys_for_v", - /* 17 */ "more_keys_for_h", - /* 18 */ "more_keys_for_j", - /* 19 */ "more_keys_for_w", - /* 20 */ "keylabel_for_nordic_row1_11", - /* 21 */ "keylabel_for_nordic_row2_10", - /* 22 */ "keylabel_for_nordic_row2_11", - /* 23 */ "more_keys_for_nordic_row2_10", - /* 24 */ "more_keys_for_nordic_row2_11", - /* 25 */ "keylabel_for_east_slavic_row1_9", - /* 26 */ "keylabel_for_east_slavic_row1_12", - /* 27 */ "keylabel_for_east_slavic_row2_1", - /* 28 */ "keylabel_for_east_slavic_row2_11", - /* 29 */ "keylabel_for_east_slavic_row3_5", - /* 30 */ "more_keys_for_cyrillic_u", - /* 31 */ "more_keys_for_cyrillic_ka", - /* 32 */ "more_keys_for_cyrillic_en", - /* 33 */ "more_keys_for_cyrillic_ghe", - /* 34 */ "more_keys_for_east_slavic_row2_1", - /* 35 */ "more_keys_for_cyrillic_a", - /* 36 */ "more_keys_for_cyrillic_o", - /* 37 */ "more_keys_for_cyrillic_soft_sign", - /* 38 */ "more_keys_for_east_slavic_row2_11", - /* 39 */ "keylabel_for_south_slavic_row1_6", - /* 40 */ "keylabel_for_south_slavic_row2_11", - /* 41 */ "keylabel_for_south_slavic_row3_1", - /* 42 */ "keylabel_for_south_slavic_row3_8", - /* 43 */ "more_keys_for_cyrillic_ie", - /* 44 */ "more_keys_for_cyrillic_i", - /* 45 */ "label_to_alpha_key", - /* 46 */ "single_quotes", - /* 47 */ "double_quotes", - /* 48 */ "single_angle_quotes", - /* 49 */ "double_angle_quotes", - /* 50 */ "more_keys_for_currency_dollar", - /* 51 */ "keylabel_for_currency", - /* 52 */ "more_keys_for_currency", - /* 53 */ "more_keys_for_punctuation", - /* 54 */ "more_keys_for_star", - /* 55 */ "more_keys_for_bullet", - /* 56 */ "more_keys_for_plus", - /* 57 */ "more_keys_for_left_parenthesis", - /* 58 */ "more_keys_for_right_parenthesis", - /* 59 */ "more_keys_for_less_than", - /* 60 */ "more_keys_for_greater_than", - /* 61 */ "more_keys_for_arabic_diacritics", - /* 62 */ "keyhintlabel_for_arabic_diacritics", - /* 63 */ "keylabel_for_symbols_1", - /* 64 */ "keylabel_for_symbols_2", - /* 65 */ "keylabel_for_symbols_3", - /* 66 */ "keylabel_for_symbols_4", - /* 67 */ "keylabel_for_symbols_5", - /* 68 */ "keylabel_for_symbols_6", - /* 69 */ "keylabel_for_symbols_7", - /* 70 */ "keylabel_for_symbols_8", - /* 71 */ "keylabel_for_symbols_9", - /* 72 */ "keylabel_for_symbols_0", - /* 73 */ "label_to_symbol_key", - /* 74 */ "label_to_symbol_with_microphone_key", - /* 75 */ "additional_more_keys_for_symbols_1", - /* 76 */ "additional_more_keys_for_symbols_2", - /* 77 */ "additional_more_keys_for_symbols_3", - /* 78 */ "additional_more_keys_for_symbols_4", - /* 79 */ "additional_more_keys_for_symbols_5", - /* 80 */ "additional_more_keys_for_symbols_6", - /* 81 */ "additional_more_keys_for_symbols_7", - /* 82 */ "additional_more_keys_for_symbols_8", - /* 83 */ "additional_more_keys_for_symbols_9", - /* 84 */ "additional_more_keys_for_symbols_0", - /* 85 */ "more_keys_for_symbols_1", - /* 86 */ "more_keys_for_symbols_2", - /* 87 */ "more_keys_for_symbols_3", - /* 88 */ "more_keys_for_symbols_4", - /* 89 */ "more_keys_for_symbols_5", - /* 90 */ "more_keys_for_symbols_6", - /* 91 */ "more_keys_for_symbols_7", - /* 92 */ "more_keys_for_symbols_8", - /* 93 */ "more_keys_for_symbols_9", - /* 94 */ "more_keys_for_symbols_0", - /* 95 */ "keylabel_for_comma", - /* 96 */ "more_keys_for_comma", - /* 97 */ "keylabel_for_symbols_question", - /* 98 */ "keylabel_for_symbols_semicolon", - /* 99 */ "keylabel_for_symbols_percent", - /* 100 */ "more_keys_for_symbols_exclamation", - /* 101 */ "more_keys_for_symbols_question", - /* 102 */ "more_keys_for_symbols_semicolon", - /* 103 */ "more_keys_for_symbols_percent", - /* 104 */ "keylabel_for_tablet_comma", - /* 105 */ "keyhintlabel_for_tablet_comma", - /* 106 */ "more_keys_for_tablet_comma", - /* 107 */ "keyhintlabel_for_period", - /* 108 */ "more_keys_for_period", - /* 109 */ "keylabel_for_apostrophe", - /* 110 */ "keyhintlabel_for_apostrophe", - /* 111 */ "more_keys_for_apostrophe", - /* 112 */ "more_keys_for_q", - /* 113 */ "more_keys_for_x", - /* 114 */ "keylabel_for_q", - /* 115 */ "keylabel_for_w", - /* 116 */ "keylabel_for_y", - /* 117 */ "keylabel_for_x", - /* 118 */ "keylabel_for_spanish_row2_10", - /* 119 */ "more_keys_for_am_pm", - /* 120 */ "settings_as_more_key", - /* 121 */ "shortcut_as_more_key", - /* 122 */ "action_next_as_more_key", - /* 123 */ "action_previous_as_more_key", - /* 124 */ "label_to_more_symbol_key", - /* 125 */ "label_to_more_symbol_for_tablet_key", - /* 126 */ "label_tab_key", - /* 127 */ "label_to_phone_numeric_key", - /* 128 */ "label_to_phone_symbols_key", - /* 129 */ "label_time_am", - /* 130 */ "label_time_pm", - /* 131 */ "keylabel_for_popular_domain", - /* 132 */ "more_keys_for_popular_domain", - /* 133 */ "more_keys_for_smiley", - /* 134 */ "single_laqm_raqm", - /* 135 */ "single_laqm_raqm_rtl", - /* 136 */ "single_raqm_laqm", - /* 137 */ "double_laqm_raqm", - /* 138 */ "double_laqm_raqm_rtl", - /* 139 */ "double_raqm_laqm", - /* 140 */ "single_lqm_rqm", - /* 141 */ "single_9qm_lqm", - /* 142 */ "single_9qm_rqm", - /* 143 */ "double_lqm_rqm", - /* 144 */ "double_9qm_lqm", - /* 145 */ "double_9qm_rqm", - /* 146 */ "more_keys_for_single_quote", - /* 147 */ "more_keys_for_double_quote", - /* 148 */ "more_keys_for_tablet_double_quote", - /* 149 */ "emoji_key_as_more_key", - }; - - private static final String EMPTY = ""; - - /* Default texts */ - private static final String[] LANGUAGE_DEFAULT = { - /* 0~ */ - EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, - EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, - EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, - EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, - /* ~44 */ - // Label for "switch to alphabetic" key. - /* 45 */ "ABC", - /* 46 */ "!text/single_lqm_rqm", - /* 47 */ "!text/double_lqm_rqm", - /* 48 */ "!text/single_laqm_raqm", - /* 49 */ "!text/double_laqm_raqm", - // U+00A2: "¢" CENT SIGN - // U+00A3: "£" POUND SIGN - // U+20AC: "€" EURO SIGN - // U+00A5: "¥" YEN SIGN - // U+20B1: "₱" PESO SIGN - /* 50 */ "\u00A2,\u00A3,\u20AC,\u00A5,\u20B1", - /* 51 */ "$", - /* 52 */ "$,\u00A2,\u20AC,\u00A3,\u00A5,\u20B1", - /* 53 */ "!fixedColumnOrder!8,;,/,(,),#,!,\\,,?,&,\\%,+,\",-,:,',@", - // U+2020: "†" DAGGER - // U+2021: "‡" DOUBLE DAGGER - // U+2605: "★" BLACK STAR - /* 54 */ "\u2020,\u2021,\u2605", - // U+266A: "♪" EIGHTH NOTE - // U+2665: "♥" BLACK HEART SUIT - // U+2660: "♠" BLACK SPADE SUIT - // U+2666: "♦" BLACK DIAMOND SUIT - // U+2663: "♣" BLACK CLUB SUIT - /* 55 */ "\u266A,\u2665,\u2660,\u2666,\u2663", - // U+00B1: "±" PLUS-MINUS SIGN - /* 56 */ "\u00B1", - // The all letters need to be mirrored are found at - // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt - /* 57 */ "!fixedColumnOrder!3,<,{,[", - /* 58 */ "!fixedColumnOrder!3,>,},]", - // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK - // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK - // U+2264: "≤" LESS-THAN OR EQUAL TO - // U+2265: "≥" GREATER-THAN EQUAL TO - // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK - // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK - /* 59 */ "!fixedColumnOrder!3,\u2039,\u2264,\u00AB", - /* 60 */ "!fixedColumnOrder!3,\u203A,\u2265,\u00BB", - /* 61 */ EMPTY, - /* 62 */ EMPTY, - /* 63 */ "1", - /* 64 */ "2", - /* 65 */ "3", - /* 66 */ "4", - /* 67 */ "5", - /* 68 */ "6", - /* 69 */ "7", - /* 70 */ "8", - /* 71 */ "9", - /* 72 */ "0", - // Label for "switch to symbols" key. - /* 73 */ "?123", - // Label for "switch to symbols with microphone" key. This string shouldn't include the "mic" - // part because it'll be appended by the code. - /* 74 */ "123", - /* 75~ */ - EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, - /* ~84 */ - // U+00B9: "¹" SUPERSCRIPT ONE - // U+00BD: "½" VULGAR FRACTION ONE HALF - // U+2153: "⅓" VULGAR FRACTION ONE THIRD - // U+00BC: "¼" VULGAR FRACTION ONE QUARTER - // U+215B: "⅛" VULGAR FRACTION ONE EIGHTH - /* 85 */ "\u00B9,\u00BD,\u2153,\u00BC,\u215B", - // U+00B2: "²" SUPERSCRIPT TWO - // U+2154: "⅔" VULGAR FRACTION TWO THIRDS - /* 86 */ "\u00B2,\u2154", - // U+00B3: "³" SUPERSCRIPT THREE - // U+00BE: "¾" VULGAR FRACTION THREE QUARTERS - // U+215C: "⅜" VULGAR FRACTION THREE EIGHTHS - /* 87 */ "\u00B3,\u00BE,\u215C", - // U+2074: "⁴" SUPERSCRIPT FOUR - /* 88 */ "\u2074", - // U+215D: "⅝" VULGAR FRACTION FIVE EIGHTHS - /* 89 */ "\u215D", - /* 90 */ EMPTY, - // U+215E: "⅞" VULGAR FRACTION SEVEN EIGHTHS - /* 91 */ "\u215E", - /* 92 */ EMPTY, - /* 93 */ EMPTY, - // U+207F: "ⁿ" SUPERSCRIPT LATIN SMALL LETTER N - // U+2205: "∅" EMPTY SET - /* 94 */ "\u207F,\u2205", - /* 95 */ ",", - /* 96 */ EMPTY, - /* 97 */ "?", - /* 98 */ ";", - /* 99 */ "%", - // U+00A1: "¡" INVERTED EXCLAMATION MARK - /* 100 */ "\u00A1", - // U+00BF: "¿" INVERTED QUESTION MARK - /* 101 */ "\u00BF", - /* 102 */ EMPTY, - // U+2030: "‰" PER MILLE SIGN - /* 103 */ "\u2030", - /* 104 */ ",", - /* 105~ */ - EMPTY, EMPTY, EMPTY, - /* ~107 */ - // U+2026: "…" HORIZONTAL ELLIPSIS - /* 108 */ "\u2026", - /* 109 */ "\'", - /* 110 */ "\"", - /* 111 */ "\"", - /* 112 */ EMPTY, - /* 113 */ EMPTY, - /* 114 */ "q", - /* 115 */ "w", - /* 116 */ "y", - /* 117 */ "x", - /* 118 */ EMPTY, - /* 119 */ "!fixedColumnOrder!2,!hasLabels!,!text/label_time_am,!text/label_time_pm", - /* 120 */ "!icon/settings_key|!code/key_settings", - /* 121 */ "!icon/shortcut_key|!code/key_shortcut", - /* 122 */ "!hasLabels!,!text/label_next_key|!code/key_action_next", - /* 123 */ "!hasLabels!,!text/label_previous_key|!code/key_action_previous", - // Label for "switch to more symbol" modifier key. Must be short to fit on key! - /* 124 */ "= \\ <", - // Label for "switch to more symbol" modifier key on tablets. Must be short to fit on key! - /* 125 */ "~ [ <", - // Label for "Tab" key. Must be short to fit on key! - /* 126 */ "Tab", - // Label for "switch to phone numeric" key. Must be short to fit on key! - /* 127 */ "123", - // Label for "switch to phone symbols" key. Must be short to fit on key! - // U+FF0A: "*" FULLWIDTH ASTERISK - // U+FF03: "#" FULLWIDTH NUMBER SIGN - /* 128 */ "\uFF0A\uFF03", - // Key label for "ante meridiem" - /* 129 */ "AM", - // Key label for "post meridiem" - /* 130 */ "PM", - /* 131 */ ".com", - // popular web domains for the locale - most popular, displayed on the keyboard - /* 132 */ "!hasLabels!,.net,.org,.gov,.edu", - /* 133 */ "!fixedColumnOrder!5,!hasLabels!,=-O|=-O ,:-P|:-P ,;-)|;-) ,:-(|:-( ,:-)|:-) ,:-!|:-! ,:-$|:-$ ,B-)|B-) ,:O|:O ,:-*|:-* ,:-D|:-D ,:\'(|:\'( ,:-\\\\|:-\\\\ ,O:-)|O:-) ,:-[|:-[ ", - // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK - // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK - // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK - // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK - // The following characters don't need BIDI mirroring. - // U+2018: "‘" LEFT SINGLE QUOTATION MARK - // U+2019: "’" RIGHT SINGLE QUOTATION MARK - // U+201A: "‚" SINGLE LOW-9 QUOTATION MARK - // U+201C: "“" LEFT DOUBLE QUOTATION MARK - // U+201D: "”" RIGHT DOUBLE QUOTATION MARK - // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK - // Abbreviations are: - // laqm: LEFT-POINTING ANGLE QUOTATION MARK - // raqm: RIGHT-POINTING ANGLE QUOTATION MARK - // rtl: Right-To-Left script order - // lqm: LEFT QUOTATION MARK - // rqm: RIGHT QUOTATION MARK - // 9qm: LOW-9 QUOTATION MARK - // The following each quotation mark pair consist of - // <opening quotation mark>, <closing quotation mark> - // and is named after (single|double)_<opening quotation mark>_<closing quotation mark>. - /* 134 */ "\u2039,\u203A", - /* 135 */ "\u2039|\u203A,\u203A|\u2039", - /* 136 */ "\u203A,\u2039", - /* 137 */ "\u00AB,\u00BB", - /* 138 */ "\u00AB|\u00BB,\u00BB|\u00AB", - /* 139 */ "\u00BB,\u00AB", - // The following each quotation mark triplet consists of - // <another quotation mark>, <opening quotation mark>, <closing quotation mark> - // and is named after (single|double)_<opening quotation mark>_<closing quotation mark>. - /* 140 */ "\u201A,\u2018,\u2019", - /* 141 */ "\u2019,\u201A,\u2018", - /* 142 */ "\u2018,\u201A,\u2019", - /* 143 */ "\u201E,\u201C,\u201D", - /* 144 */ "\u201D,\u201E,\u201C", - /* 145 */ "\u201C,\u201E,\u201D", - /* 146 */ "!fixedColumnOrder!5,!text/single_quotes,!text/single_angle_quotes", - /* 147 */ "!fixedColumnOrder!5,!text/double_quotes,!text/double_angle_quotes", - /* 148 */ "!fixedColumnOrder!6,!text/double_quotes,!text/single_quotes,!text/double_angle_quotes,!text/single_angle_quotes", - /* 149 */ "!icon/emoji_key|!code/key_emoji", - }; - - /* Language af: Afrikaans */ - private static final String[] LANGUAGE_af = { - // This is the same as Dutch except more keys of y and demoting vowels with diaeresis. - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* 0 */ "\u00E1,\u00E2,\u00E4,\u00E0,\u00E6,\u00E3,\u00E5,\u0101", - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - /* 1 */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113", - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - // U+0133: "ij" LATIN SMALL LIGATURE IJ - /* 2 */ "\u00ED,\u00EC,\u00EF,\u00EE,\u012F,\u012B,\u0133", - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - /* 3 */ "\u00F3,\u00F4,\u00F6,\u00F2,\u00F5,\u0153,\u00F8,\u014D", - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* 4 */ "\u00FA,\u00FB,\u00FC,\u00F9,\u016B", - /* 5 */ null, - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* 6 */ "\u00F1,\u0144", - /* 7 */ null, - // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE - // U+0133: "ij" LATIN SMALL LIGATURE IJ - /* 8 */ "\u00FD,\u0133", - }; - - /* Language ar: Arabic */ - private static final String[] LANGUAGE_ar = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~44 */ - // Label for "switch to alphabetic" key. - // U+0623: "ا" ARABIC LETTER ALEF - // U+200C: ZERO WIDTH NON-JOINER - // U+0628: "ب" ARABIC LETTER BEH - // U+062C: "پ" ARABIC LETTER PEH - /* 45 */ "\u0623\u200C\u0628\u200C\u062C", - /* 46 */ null, - /* 47 */ null, - /* 48 */ "!text/single_laqm_raqm_rtl", - /* 49 */ "!text/double_laqm_raqm_rtl", - /* 50~ */ - null, null, null, - /* ~52 */ - // U+061F: "؟" ARABIC QUESTION MARK - // U+060C: "،" ARABIC COMMA - // U+061B: "؛" ARABIC SEMICOLON - /* 53 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(|),)|(", - // U+2605: "★" BLACK STAR - // U+066D: "٭" ARABIC FIVE POINTED STAR - /* 54 */ "\u2605,\u066D", - // U+266A: "♪" EIGHTH NOTE - /* 55 */ "\u266A", - /* 56 */ null, - // The all letters need to be mirrored are found at - // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt - // U+FD3E: "﴾" ORNATE LEFT PARENTHESIS - // U+FD3F: "﴿" ORNATE RIGHT PARENTHESIS - /* 57 */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,<|>,{|},[|]", - /* 58 */ "!fixedColumnOrder!4,\uFD3F|\uFD3E,>|<,}|{,]|[", - // U+2264: "≤" LESS-THAN OR EQUAL TO - // U+2265: "≥" GREATER-THAN EQUAL TO - // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK - // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK - // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK - // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK - /* 59 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,\u00AB|\u00BB", - /* 60 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,\u00BB|\u00AB", - // U+0655: "ٕ" ARABIC HAMZA BELOW - // U+0654: "ٔ" ARABIC HAMZA ABOVE - // U+0652: "ْ" ARABIC SUKUN - // U+064D: "ٍ" ARABIC KASRATAN - // U+064C: "ٌ" ARABIC DAMMATAN - // U+064B: "ً" ARABIC FATHATAN - // U+0651: "ّ" ARABIC SHADDA - // U+0656: "ٖ" ARABIC SUBSCRIPT ALEF - // U+0670: "ٰ" ARABIC LETTER SUPERSCRIPT ALEF - // U+0653: "ٓ" ARABIC MADDAH ABOVE - // U+0650: "ِ" ARABIC KASRA - // U+064F: "ُ" ARABIC DAMMA - // U+064E: "َ" ARABIC FATHA - // U+0640: "ـ" ARABIC TATWEEL - // In order to make Tatweel easily distinguishable from other punctuations, we use consecutive Tatweels only for its displayed label. - // Note: The space character is needed as a preceding letter to draw Arabic diacritics characters correctly. - /* 61 */ "!fixedColumnOrder!7, \u0655|\u0655, \u0654|\u0654, \u0652|\u0652, \u064D|\u064D, \u064C|\u064C, \u064B|\u064B, \u0651|\u0651, \u0656|\u0656, \u0670|\u0670, \u0653|\u0653, \u0650|\u0650, \u064F|\u064F, \u064E|\u064E,\u0640\u0640\u0640|\u0640", - /* 62 */ "\u0651", - // U+0661: "١" ARABIC-INDIC DIGIT ONE - /* 63 */ "\u0661", - // U+0662: "٢" ARABIC-INDIC DIGIT TWO - /* 64 */ "\u0662", - // U+0663: "٣" ARABIC-INDIC DIGIT THREE - /* 65 */ "\u0663", - // U+0664: "٤" ARABIC-INDIC DIGIT FOUR - /* 66 */ "\u0664", - // U+0665: "٥" ARABIC-INDIC DIGIT FIVE - /* 67 */ "\u0665", - // U+0666: "٦" ARABIC-INDIC DIGIT SIX - /* 68 */ "\u0666", - // U+0667: "٧" ARABIC-INDIC DIGIT SEVEN - /* 69 */ "\u0667", - // U+0668: "٨" ARABIC-INDIC DIGIT EIGHT - /* 70 */ "\u0668", - // U+0669: "٩" ARABIC-INDIC DIGIT NINE - /* 71 */ "\u0669", - // U+0660: "٠" ARABIC-INDIC DIGIT ZERO - /* 72 */ "\u0660", - // Label for "switch to symbols" key. - // U+061F: "؟" ARABIC QUESTION MARK - /* 73 */ "\u0663\u0662\u0661\u061F", - // Label for "switch to symbols with microphone" key. This string shouldn't include the "mic" - // part because it'll be appended by the code. - /* 74 */ "\u0663\u0662\u0661", - /* 75 */ "1", - /* 76 */ "2", - /* 77 */ "3", - /* 78 */ "4", - /* 79 */ "5", - /* 80 */ "6", - /* 81 */ "7", - /* 82 */ "8", - /* 83 */ "9", - // U+066B: "٫" ARABIC DECIMAL SEPARATOR - // U+066C: "٬" ARABIC THOUSANDS SEPARATOR - /* 84 */ "0,\u066B,\u066C", - /* 85~ */ - null, null, null, null, null, null, null, null, null, null, - /* ~94 */ - // U+060C: "،" ARABIC COMMA - /* 95 */ "\u060C", - /* 96 */ "\\,", - /* 97 */ "\u061F", - /* 98 */ "\u061B", - // U+066A: "٪" ARABIC PERCENT SIGN - /* 99 */ "\u066A", - /* 100 */ null, - /* 101 */ "?", - /* 102 */ ";", - // U+2030: "‰" PER MILLE SIGN - /* 103 */ "\\%,\u2030", - /* 104~ */ - null, null, null, null, null, - /* ~108 */ - // U+060C: "،" ARABIC COMMA - // U+061B: "؛" ARABIC SEMICOLON - // U+061F: "؟" ARABIC QUESTION MARK - /* 109 */ "\u060C", - /* 110 */ "\u061F", - /* 111 */ "\u061F,\u061B,!,:,-,/,\',\"", - }; - - /* Language az: Azerbaijani */ - private static final String[] LANGUAGE_az = { - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - /* 0 */ "\u00E2", - // U+0259: "ə" LATIN SMALL LETTER SCHWA - /* 1 */ "\u0259", - // U+0131: "ı" LATIN SMALL LETTER DOTLESS I - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - /* 2 */ "\u0131,\u00EE,\u00EF,\u00EC,\u00ED,\u012F,\u012B", - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - /* 3 */ "\u00F6,\u00F4,\u0153,\u00F2,\u00F3,\u00F5,\u00F8,\u014D", - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* 4 */ "\u00FC,\u00FB,\u00F9,\u00FA,\u016B", - // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - /* 5 */ "\u015F,\u00DF,\u015B,\u0161", - /* 6 */ null, - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE - // U+010D: "č" LATIN SMALL LETTER C WITH CARON - /* 7 */ "\u00E7,\u0107,\u010D", - /* 8~ */ - null, null, null, null, null, null, null, - /* ~14 */ - // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE - /* 15 */ "\u011F", - }; - - /* Language be: Belarusian */ - private static final String[] LANGUAGE_be = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, - /* ~24 */ - // U+045E: "ў" CYRILLIC SMALL LETTER SHORT U - /* 25 */ "\u045E", - // U+0451: "ё" CYRILLIC SMALL LETTER IO - /* 26 */ "\u0451", - // U+044B: "ы" CYRILLIC SMALL LETTER YERU - /* 27 */ "\u044B", - // U+044D: "э" CYRILLIC SMALL LETTER E - /* 28 */ "\u044D", - // U+0456: "і" CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I - /* 29 */ "\u0456", - /* 30~ */ - null, null, null, null, null, null, null, - /* ~36 */ - // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN - /* 37 */ "\u044A", - /* 38~ */ - null, null, null, null, null, - /* ~42 */ - // U+0451: "ё" CYRILLIC SMALL LETTER IO - /* 43 */ "\u0451", - /* 44 */ null, - // Label for "switch to alphabetic" key. - // U+0410: "А" CYRILLIC CAPITAL LETTER A - // U+0411: "Б" CYRILLIC CAPITAL LETTER BE - // U+0412: "В" CYRILLIC CAPITAL LETTER VE - /* 45 */ "\u0410\u0411\u0412", - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - }; - - /* Language bg: Bulgarian */ - private static final String[] LANGUAGE_bg = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~44 */ - // Label for "switch to alphabetic" key. - // U+0410: "А" CYRILLIC CAPITAL LETTER A - // U+0411: "Б" CYRILLIC CAPITAL LETTER BE - // U+0412: "В" CYRILLIC CAPITAL LETTER VE - /* 45 */ "\u0410\u0411\u0412", - /* 46 */ null, - // single_quotes of Bulgarian is default single_quotes_right_left. - /* 47 */ "!text/double_9qm_lqm", - }; - - /* Language ca: Catalan */ - private static final String[] LANGUAGE_ca = { - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - // U+00AA: "ª" FEMININE ORDINAL INDICATOR - /* 0 */ "\u00E0,\u00E1,\u00E4,\u00E2,\u00E3,\u00E5,\u0105,\u00E6,\u0101,\u00AA", - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - /* 1 */ "\u00E8,\u00E9,\u00EB,\u00EA,\u0119,\u0117,\u0113", - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - /* 2 */ "\u00ED,\u00EF,\u00EC,\u00EE,\u012F,\u012B", - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - // U+00BA: "º" MASCULINE ORDINAL INDICATOR - /* 3 */ "\u00F2,\u00F3,\u00F6,\u00F4,\u00F5,\u00F8,\u0153,\u014D,\u00BA", - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* 4 */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", - /* 5 */ null, - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* 6 */ "\u00F1,\u0144", - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE - // U+010D: "č" LATIN SMALL LETTER C WITH CARON - /* 7 */ "\u00E7,\u0107,\u010D", - /* 8~ */ - null, null, null, null, null, null, - /* ~13 */ - // U+00B7: "·" MIDDLE DOT - // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE - /* 14 */ "l\u00B7l,\u0142", - /* 15~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, - /* ~52 */ - // U+00B7: "·" MIDDLE DOT - /* 53 */ "!fixedColumnOrder!9,;,/,(,),#,\u00B7,!,\\,,?,&,\\%,+,\",-,:,',@", - /* 54~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, - /* ~107 */ - /* 108 */ "?,\u00B7", - /* 109~ */ - null, null, null, null, null, null, null, null, null, - /* ~117 */ - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - /* 118 */ "\u00E7", - }; - - /* Language cs: Czech */ - private static final String[] LANGUAGE_cs = { - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* 0 */ "\u00E1,\u00E0,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101", - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+011B: "ě" LATIN SMALL LETTER E WITH CARON - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - /* 1 */ "\u00E9,\u011B,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113", - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - /* 2 */ "\u00ED,\u00EE,\u00EF,\u00EC,\u012F,\u012B", - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - /* 3 */ "\u00F3,\u00F6,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D", - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* 4 */ "\u00FA,\u016F,\u00FB,\u00FC,\u00F9,\u016B", - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE - /* 5 */ "\u0161,\u00DF,\u015B", - // U+0148: "ň" LATIN SMALL LETTER N WITH CARON - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* 6 */ "\u0148,\u00F1,\u0144", - // U+010D: "č" LATIN SMALL LETTER C WITH CARON - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE - /* 7 */ "\u010D,\u00E7,\u0107", - // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE - // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS - /* 8 */ "\u00FD,\u00FF", - // U+010F: "ď" LATIN SMALL LETTER D WITH CARON - /* 9 */ "\u010F", - // U+0159: "ř" LATIN SMALL LETTER R WITH CARON - /* 10 */ "\u0159", - // U+0165: "ť" LATIN SMALL LETTER T WITH CARON - /* 11 */ "\u0165", - // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON - // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE - // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE - /* 12 */ "\u017E,\u017A,\u017C", - /* 13~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - /* 48 */ "!text/single_raqm_laqm", - /* 49 */ "!text/double_raqm_laqm", - }; - - /* Language da: Danish */ - private static final String[] LANGUAGE_da = { - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* 0 */ "\u00E1,\u00E4,\u00E0,\u00E2,\u00E3,\u0101", - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - /* 1 */ "\u00E9,\u00EB", - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - /* 2 */ "\u00ED,\u00EF", - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - /* 3 */ "\u00F3,\u00F4,\u00F2,\u00F5,\u0153,\u014D", - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* 4 */ "\u00FA,\u00FC,\u00FB,\u00F9,\u016B", - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - /* 5 */ "\u00DF,\u015B,\u0161", - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* 6 */ "\u00F1,\u0144", - /* 7 */ null, - // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE - // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS - /* 8 */ "\u00FD,\u00FF", - // U+00F0: "ð" LATIN SMALL LETTER ETH - /* 9 */ "\u00F0", - /* 10~ */ - null, null, null, null, - /* ~13 */ - // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE - /* 14 */ "\u0142", - /* 15~ */ - null, null, null, null, null, - /* ~19 */ - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - /* 20 */ "\u00E5", - // U+00E6: "æ" LATIN SMALL LETTER AE - /* 21 */ "\u00E6", - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - /* 22 */ "\u00F8", - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - /* 23 */ "\u00E4", - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - /* 24 */ "\u00F6", - /* 25~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - /* 48 */ "!text/single_raqm_laqm", - /* 49 */ "!text/double_raqm_laqm", - }; - - /* Language de: German */ - private static final String[] LANGUAGE_de = { - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* 0 */ "\u00E4,\u00E2,\u00E0,\u00E1,\u00E6,\u00E3,\u00E5,\u0101", - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - /* 1 */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0117", - /* 2 */ null, - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - /* 3 */ "\u00F6,\u00F4,\u00F2,\u00F3,\u00F5,\u0153,\u00F8,\u014D", - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* 4 */ "\u00FC,\u00FB,\u00F9,\u00FA,\u016B", - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - /* 5 */ "\u00DF,\u015B,\u0161", - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* 6 */ "\u00F1,\u0144", - /* 7~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - /* 48 */ "!text/single_raqm_laqm", - /* 49 */ "!text/double_raqm_laqm", - }; - - /* Language el: Greek */ - private static final String[] LANGUAGE_el = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~44 */ - // Label for "switch to alphabetic" key. - // U+0391: "Α" GREEK CAPITAL LETTER ALPHA - // U+0392: "Β" GREEK CAPITAL LETTER BETA - // U+0393: "Γ" GREEK CAPITAL LETTER GAMMA - /* 45 */ "\u0391\u0392\u0393", - }; - - /* Language en: English */ - private static final String[] LANGUAGE_en = { - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* 0 */ "\u00E0,\u00E1,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101", - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - /* 1 */ "\u00E8,\u00E9,\u00EA,\u00EB,\u0113", - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - /* 2 */ "\u00EE,\u00EF,\u00ED,\u012B,\u00EC", - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - /* 3 */ "\u00F4,\u00F6,\u00F2,\u00F3,\u0153,\u00F8,\u014D,\u00F5", - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* 4 */ "\u00FB,\u00FC,\u00F9,\u00FA,\u016B", - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - /* 5 */ "\u00DF", - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - /* 6 */ "\u00F1", - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - /* 7 */ "\u00E7", - }; - - /* Language eo: Esperanto */ - private static final String[] LANGUAGE_eo = { - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - // U+0103: "ă" LATIN SMALL LETTER A WITH BREVE - // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK - // U+00AA: "ª" FEMININE ORDINAL INDICATOR - /* 0 */ "\u00E1,\u00E0,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101,\u0103,\u0105,\u00AA", - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+011B: "ě" LATIN SMALL LETTER E WITH CARON - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - /* 1 */ "\u00E9,\u011B,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113", - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+0129: "ĩ" LATIN SMALL LETTER I WITH TILDE - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - // U+0131: "ı" LATIN SMALL LETTER DOTLESS I - // U+0133: "ij" LATIN SMALL LIGATURE IJ - /* 2 */ "\u00ED,\u00EE,\u00EF,\u0129,\u00EC,\u012F,\u012B,\u0131,\u0133", - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE - // U+00BA: "º" MASCULINE ORDINAL INDICATOR - /* 3 */ "\u00F3,\u00F6,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D,\u0151,\u00BA", - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - // U+0169: "ũ" LATIN SMALL LETTER U WITH TILDE - // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE - // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK - // U+00B5: "µ" MICRO SIGN - /* 4 */ "\u00FA,\u016F,\u00FB,\u00FC,\u00F9,\u016B,\u0169,\u0171,\u0173,\u00B5", - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE - // U+0219: "ș" LATIN SMALL LETTER S WITH COMMA BELOW - // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA - /* 5 */ "\u00DF,\u0161,\u015B,\u0219,\u015F", - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA - // U+0148: "ň" LATIN SMALL LETTER N WITH CARON - // U+0149: "ʼn" LATIN SMALL LETTER N PRECEDED BY APOSTROPHE - // U+014B: "ŋ" LATIN SMALL LETTER ENG - /* 6 */ "\u00F1,\u0144,\u0146,\u0148,\u0149,\u014B", - // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE - // U+010D: "č" LATIN SMALL LETTER C WITH CARON - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - // U+010B: "ċ" LATIN SMALL LETTER C WITH DOT ABOVE - /* 7 */ "\u0107,\u010D,\u00E7,\u010B", - // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE - // U+0177: "ŷ" LATIN SMALL LETTER Y WITH CIRCUMFLEX - // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS - // U+00FE: "þ" LATIN SMALL LETTER THORN - /* 8 */ "y,\u00FD,\u0177,\u00FF,\u00FE", - // U+00F0: "ð" LATIN SMALL LETTER ETH - // U+010F: "ď" LATIN SMALL LETTER D WITH CARON - // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE - /* 9 */ "\u00F0,\u010F,\u0111", - // U+0159: "ř" LATIN SMALL LETTER R WITH CARON - // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE - // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA - /* 10 */ "\u0159,\u0155,\u0157", - // U+0165: "ť" LATIN SMALL LETTER T WITH CARON - // U+021B: "ț" LATIN SMALL LETTER T WITH COMMA BELOW - // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA - // U+0167: "ŧ" LATIN SMALL LETTER T WITH STROKE - /* 11 */ "\u0165,\u021B,\u0163,\u0167", - // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE - // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE - // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON - /* 12 */ "\u017A,\u017C,\u017E", - // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA - // U+0138: "ĸ" LATIN SMALL LETTER KRA - /* 13 */ "\u0137,\u0138", - // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE - // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA - // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON - // U+0140: "ŀ" LATIN SMALL LETTER L WITH MIDDLE DOT - // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE - /* 14 */ "\u013A,\u013C,\u013E,\u0140,\u0142", - // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE - // U+0121: "ġ" LATIN SMALL LETTER G WITH DOT ABOVE - // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA - /* 15 */ "\u011F,\u0121,\u0123", - // U+0175: "ŵ" LATIN SMALL LETTER W WITH CIRCUMFLEX - /* 16 */ "w,\u0175", - // U+0125: "ĥ" LATIN SMALL LETTER H WITH CIRCUMFLEX - // U+0127: "ħ" LATIN SMALL LETTER H WITH STROKE - /* 17 */ "\u0125,\u0127", - /* 18 */ null, - // U+0175: "ŵ" LATIN SMALL LETTER W WITH CIRCUMFLEX - /* 19 */ "w,\u0175", - /* 20~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, - /* ~111 */ - /* 112 */ "q", - /* 113 */ "x", - // U+015D: "ŝ" LATIN SMALL LETTER S WITH CIRCUMFLEX - /* 114 */ "\u015D", - // U+011D: "ĝ" LATIN SMALL LETTER G WITH CIRCUMFLEX - /* 115 */ "\u011D", - // U+016D: "ŭ" LATIN SMALL LETTER U WITH BREVE - /* 116 */ "\u016D", - // U+0109: "ĉ" LATIN SMALL LETTER C WITH CIRCUMFLEX - /* 117 */ "\u0109", - // U+0135: "ĵ" LATIN SMALL LETTER J WITH CIRCUMFLEX - /* 118 */ "\u0135", - }; - - /* Language es: Spanish */ - private static final String[] LANGUAGE_es = { - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - // U+00AA: "ª" FEMININE ORDINAL INDICATOR - /* 0 */ "\u00E1,\u00E0,\u00E4,\u00E2,\u00E3,\u00E5,\u0105,\u00E6,\u0101,\u00AA", - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - /* 1 */ "\u00E9,\u00E8,\u00EB,\u00EA,\u0119,\u0117,\u0113", - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - /* 2 */ "\u00ED,\u00EF,\u00EC,\u00EE,\u012F,\u012B", - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - // U+00BA: "º" MASCULINE ORDINAL INDICATOR - /* 3 */ "\u00F3,\u00F2,\u00F6,\u00F4,\u00F5,\u00F8,\u0153,\u014D,\u00BA", - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* 4 */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", - /* 5 */ null, - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* 6 */ "\u00F1,\u0144", - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE - // U+010D: "č" LATIN SMALL LETTER C WITH CARON - /* 7 */ "\u00E7,\u0107,\u010D", - /* 8~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~52 */ - // U+00A1: "¡" INVERTED EXCLAMATION MARK - // U+00BF: "¿" INVERTED QUESTION MARK - /* 53 */ "!fixedColumnOrder!4,;,!,\\,,?,:,\u00A1,@,\u00BF", - /* 54~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, - /* ~105 */ - // U+00A1: "¡" INVERTED EXCLAMATION MARK - /* 106 */ "!,\u00A1", - /* 107 */ null, - // U+00BF: "¿" INVERTED QUESTION MARK - /* 108 */ "?,\u00BF", - /* 109 */ "\"", - /* 110 */ "\'", - /* 111 */ "\'", - /* 112~ */ - null, null, null, null, null, null, - /* ~117 */ - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - /* 118 */ "\u00F1", - }; - - /* Language et: Estonian */ - private static final String[] LANGUAGE_et = { - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK - /* 0 */ "\u00E4,\u0101,\u00E0,\u00E1,\u00E2,\u00E3,\u00E5,\u00E6,\u0105", - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - // U+011B: "ě" LATIN SMALL LETTER E WITH CARON - /* 1 */ "\u0113,\u00E8,\u0117,\u00E9,\u00EA,\u00EB,\u0119,\u011B", - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+0131: "ı" LATIN SMALL LETTER DOTLESS I - /* 2 */ "\u012B,\u00EC,\u012F,\u00ED,\u00EE,\u00EF,\u0131", - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - /* 3 */ "\u00F6,\u00F5,\u00F2,\u00F3,\u00F4,\u0153,\u0151,\u00F8", - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE - // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE - /* 4 */ "\u00FC,\u016B,\u0173,\u00F9,\u00FA,\u00FB,\u016F,\u0171", - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE - // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA - /* 5 */ "\u0161,\u00DF,\u015B,\u015F", - // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* 6 */ "\u0146,\u00F1,\u0144,\u0144", - // U+010D: "č" LATIN SMALL LETTER C WITH CARON - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE - /* 7 */ "\u010D,\u00E7,\u0107", - // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE - // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS - /* 8 */ "\u00FD,\u00FF", - // U+010F: "ď" LATIN SMALL LETTER D WITH CARON - /* 9 */ "\u010F", - // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA - // U+0159: "ř" LATIN SMALL LETTER R WITH CARON - // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE - /* 10 */ "\u0157,\u0159,\u0155", - // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA - // U+0165: "ť" LATIN SMALL LETTER T WITH CARON - /* 11 */ "\u0163,\u0165", - // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON - // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE - // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE - /* 12 */ "\u017E,\u017C,\u017A", - // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA - /* 13 */ "\u0137", - // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA - // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE - // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE - // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON - /* 14 */ "\u013C,\u0142,\u013A,\u013E", - // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA - // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE - /* 15 */ "\u0123,\u011F", - /* 16~ */ - null, null, null, null, - /* ~19 */ - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - /* 20 */ "\u00FC", - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - /* 21 */ "\u00F6", - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - /* 22 */ "\u00E4", - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - /* 23 */ "\u00F5", - /* 24~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - }; - - /* Language fa: Persian */ - private static final String[] LANGUAGE_fa = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~44 */ - // Label for "switch to alphabetic" key. - // U+0627: "ا" ARABIC LETTER ALEF - // U+200C: ZERO WIDTH NON-JOINER - // U+0628: "ب" ARABIC LETTER BEH - // U+067E: "پ" ARABIC LETTER PEH - /* 45 */ "\u0627\u200C\u0628\u200C\u067E", - /* 46 */ null, - /* 47 */ null, - /* 48 */ "!text/single_laqm_raqm_rtl", - /* 49 */ "!text/double_laqm_raqm_rtl", - /* 50 */ null, - // U+FDFC: "﷼" RIAL SIGN - /* 51 */ "\uFDFC", - /* 52 */ null, - // U+061F: "؟" ARABIC QUESTION MARK - // U+060C: "،" ARABIC COMMA - // U+061B: "؛" ARABIC SEMICOLON - /* 53 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(|),)|(", - // U+2605: "★" BLACK STAR - // U+066D: "٭" ARABIC FIVE POINTED STAR - /* 54 */ "\u2605,\u066D", - // U+266A: "♪" EIGHTH NOTE - /* 55 */ "\u266A", - /* 56 */ null, - // The all letters need to be mirrored are found at - // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt - // U+FD3E: "﴾" ORNATE LEFT PARENTHESIS - // U+FD3F: "﴿" ORNATE RIGHT PARENTHESIS - /* 57 */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,<|>,{|},[|]", - /* 58 */ "!fixedColumnOrder!4,\uFD3F|\uFD3E,>|<,}|{,]|[", - // U+2264: "≤" LESS-THAN OR EQUAL TO - // U+2265: "≥" GREATER-THAN EQUAL TO - // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK - // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK - // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK - // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK - /* 59 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,<|>", - /* 60 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,>|<", - // U+0655: "ٕ" ARABIC HAMZA BELOW - // U+0652: "ْ" ARABIC SUKUN - // U+0651: "ّ" ARABIC SHADDA - // U+064C: "ٌ" ARABIC DAMMATAN - // U+064D: "ٍ" ARABIC KASRATAN - // U+064B: "ً" ARABIC FATHATAN - // U+0654: "ٔ" ARABIC HAMZA ABOVE - // U+0656: "ٖ" ARABIC SUBSCRIPT ALEF - // U+0670: "ٰ" ARABIC LETTER SUPERSCRIPT ALEF - // U+0653: "ٓ" ARABIC MADDAH ABOVE - // U+064F: "ُ" ARABIC DAMMA - // U+0650: "ِ" ARABIC KASRA - // U+064E: "َ" ARABIC FATHA - // U+0640: "ـ" ARABIC TATWEEL - // In order to make Tatweel easily distinguishable from other punctuations, we use consecutive Tatweels only for its displayed label. - // Note: The space character is needed as a preceding letter to draw Arabic diacritics characters correctly. - /* 61 */ "!fixedColumnOrder!7, \u0655|\u0655, \u0652|\u0652, \u0651|\u0651, \u064C|\u064C, \u064D|\u064D, \u064B|\u064B, \u0654|\u0654, \u0656|\u0656, \u0670|\u0670, \u0653|\u0653, \u064F|\u064F, \u0650|\u0650, \u064E|\u064E,\u0640\u0640\u0640|\u0640", - /* 62 */ "\u064B", - // U+06F1: "۱" EXTENDED ARABIC-INDIC DIGIT ONE - /* 63 */ "\u06F1", - // U+06F2: "۲" EXTENDED ARABIC-INDIC DIGIT TWO - /* 64 */ "\u06F2", - // U+06F3: "۳" EXTENDED ARABIC-INDIC DIGIT THREE - /* 65 */ "\u06F3", - // U+06F4: "۴" EXTENDED ARABIC-INDIC DIGIT FOUR - /* 66 */ "\u06F4", - // U+06F5: "۵" EXTENDED ARABIC-INDIC DIGIT FIVE - /* 67 */ "\u06F5", - // U+06F6: "۶" EXTENDED ARABIC-INDIC DIGIT SIX - /* 68 */ "\u06F6", - // U+06F7: "۷" EXTENDED ARABIC-INDIC DIGIT SEVEN - /* 69 */ "\u06F7", - // U+06F8: "۸" EXTENDED ARABIC-INDIC DIGIT EIGHT - /* 70 */ "\u06F8", - // U+06F9: "۹" EXTENDED ARABIC-INDIC DIGIT NINE - /* 71 */ "\u06F9", - // U+06F0: "۰" EXTENDED ARABIC-INDIC DIGIT ZERO - /* 72 */ "\u06F0", - // Label for "switch to symbols" key. - // U+061F: "؟" ARABIC QUESTION MARK - /* 73 */ "\u06F3\u06F2\u06F1\u061F", - // Label for "switch to symbols with microphone" key. This string shouldn't include the "mic" - // part because it'll be appended by the code. - /* 74 */ "\u06F3\u06F2\u06F1", - /* 75 */ "1", - /* 76 */ "2", - /* 77 */ "3", - /* 78 */ "4", - /* 79 */ "5", - /* 80 */ "6", - /* 81 */ "7", - /* 82 */ "8", - /* 83 */ "9", - // U+066B: "٫" ARABIC DECIMAL SEPARATOR - // U+066C: "٬" ARABIC THOUSANDS SEPARATOR - /* 84 */ "0,\u066B,\u066C", - /* 85~ */ - null, null, null, null, null, null, null, null, null, null, - /* ~94 */ - // U+060C: "،" ARABIC COMMA - /* 95 */ "\u060C", - /* 96 */ "\\,", - /* 97 */ "\u061F", - /* 98 */ "\u061B", - // U+066A: "٪" ARABIC PERCENT SIGN - /* 99 */ "\u066A", - /* 100 */ null, - /* 101 */ "?", - /* 102 */ ";", - // U+2030: "‰" PER MILLE SIGN - /* 103 */ "\\%,\u2030", - // U+060C: "،" ARABIC COMMA - // U+061B: "؛" ARABIC SEMICOLON - // U+061F: "؟" ARABIC QUESTION MARK - // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK - // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK - /* 104 */ "\u060C", - /* 105 */ "!", - /* 106 */ "!,\\,", - /* 107 */ "\u061F", - /* 108 */ "\u061F,?", - /* 109 */ "\u060C", - /* 110 */ "\u061F", - /* 111 */ "!fixedColumnOrder!4,:,!,\u061F,\u061B,-,/,\u00AB|\u00BB,\u00BB|\u00AB", - }; - - /* Language fi: Finnish */ - private static final String[] LANGUAGE_fi = { - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* 0 */ "\u00E6,\u00E0,\u00E1,\u00E2,\u00E3,\u0101", - /* 1 */ null, - /* 2 */ null, - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - /* 3 */ "\u00F8,\u00F4,\u00F2,\u00F3,\u00F5,\u0153,\u014D", - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - /* 4 */ "\u00FC", - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE - /* 5 */ "\u0161,\u00DF,\u015B", - /* 6~ */ - null, null, null, null, null, null, - /* ~11 */ - // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON - // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE - // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE - /* 12 */ "\u017E,\u017A,\u017C", - /* 13~ */ - null, null, null, null, null, null, null, - /* ~19 */ - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - /* 20 */ "\u00E5", - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - /* 21 */ "\u00F6", - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - /* 22 */ "\u00E4", - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - /* 23 */ "\u00F8", - // U+00E6: "æ" LATIN SMALL LETTER AE - /* 24 */ "\u00E6", - }; - - /* Language fr: French */ - private static final String[] LANGUAGE_fr = { - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - // U+00AA: "ª" FEMININE ORDINAL INDICATOR - /* 0 */ "\u00E0,\u00E2,%,\u00E6,\u00E1,\u00E4,\u00E3,\u00E5,\u0101,\u00AA", - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - /* 1 */ "\u00E9,\u00E8,\u00EA,\u00EB,%,\u0119,\u0117,\u0113", - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - /* 2 */ "\u00EE,%,\u00EF,\u00EC,\u00ED,\u012F,\u012B", - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - // U+00BA: "º" MASCULINE ORDINAL INDICATOR - /* 3 */ "\u00F4,\u0153,%,\u00F6,\u00F2,\u00F3,\u00F5,\u00F8,\u014D,\u00BA", - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* 4 */ "\u00F9,\u00FB,%,\u00FC,\u00FA,\u016B", - /* 5 */ null, - /* 6 */ null, - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE - // U+010D: "č" LATIN SMALL LETTER C WITH CARON - /* 7 */ "\u00E7,\u0107,\u010D", - // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS - /* 8 */ "%,\u00FF", - }; - - /* Language hi: Hindi */ - private static final String[] LANGUAGE_hi = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~44 */ - // Label for "switch to alphabetic" key. - // U+0915: "क" DEVANAGARI LETTER KA - // U+0916: "ख" DEVANAGARI LETTER KHA - // U+0917: "ग" DEVANAGARI LETTER GA - /* 45 */ "\u0915\u0916\u0917", - /* 46~ */ - null, null, null, null, null, - /* ~50 */ - // U+20B9: "₹" INDIAN RUPEE SIGN - /* 51 */ "\u20B9", - /* 52~ */ - null, null, null, null, null, null, null, null, null, null, null, - /* ~62 */ - // U+0967: "१" DEVANAGARI DIGIT ONE - /* 63 */ "\u0967", - // U+0968: "२" DEVANAGARI DIGIT TWO - /* 64 */ "\u0968", - // U+0969: "३" DEVANAGARI DIGIT THREE - /* 65 */ "\u0969", - // U+096A: "४" DEVANAGARI DIGIT FOUR - /* 66 */ "\u096A", - // U+096B: "५" DEVANAGARI DIGIT FIVE - /* 67 */ "\u096B", - // U+096C: "६" DEVANAGARI DIGIT SIX - /* 68 */ "\u096C", - // U+096D: "७" DEVANAGARI DIGIT SEVEN - /* 69 */ "\u096D", - // U+096E: "८" DEVANAGARI DIGIT EIGHT - /* 70 */ "\u096E", - // U+096F: "९" DEVANAGARI DIGIT NINE - /* 71 */ "\u096F", - // U+0966: "०" DEVANAGARI DIGIT ZERO - /* 72 */ "\u0966", - // Label for "switch to symbols" key. - /* 73 */ "?\u0967\u0968\u0969", - // Label for "switch to symbols with microphone" key. This string shouldn't include the "mic" - // part because it'll be appended by the code. - /* 74 */ "\u0967\u0968\u0969", - /* 75 */ "1", - /* 76 */ "2", - /* 77 */ "3", - /* 78 */ "4", - /* 79 */ "5", - /* 80 */ "6", - /* 81 */ "7", - /* 82 */ "8", - /* 83 */ "9", - /* 84 */ "0", - }; - - /* Language hr: Croatian */ - private static final String[] LANGUAGE_hr = { - /* 0~ */ - null, null, null, null, null, - /* ~4 */ - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - /* 5 */ "\u0161,\u015B,\u00DF", - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* 6 */ "\u00F1,\u0144", - // U+010D: "č" LATIN SMALL LETTER C WITH CARON - // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - /* 7 */ "\u010D,\u0107,\u00E7", - /* 8 */ null, - // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE - /* 9 */ "\u0111", - /* 10 */ null, - /* 11 */ null, - // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON - // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE - // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE - /* 12 */ "\u017E,\u017A,\u017C", - /* 13~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_rqm", - /* 47 */ "!text/double_9qm_rqm", - /* 48 */ "!text/single_raqm_laqm", - /* 49 */ "!text/double_raqm_laqm", - }; - - /* Language hu: Hungarian */ - private static final String[] LANGUAGE_hu = { - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* 0 */ "\u00E1,\u00E0,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101", - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - /* 1 */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113", - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - /* 2 */ "\u00ED,\u00EE,\u00EF,\u00EC,\u012F,\u012B", - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - /* 3 */ "\u00F3,\u00F6,\u0151,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D", - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* 4 */ "\u00FA,\u00FC,\u0171,\u00FB,\u00F9,\u016B", - /* 5~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_rqm", - /* 47 */ "!text/double_9qm_rqm", - /* 48 */ "!text/single_raqm_laqm", - /* 49 */ "!text/double_raqm_laqm", - }; - - /* Language hy: Armenian */ - private static final String[] LANGUAGE_hy = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, - /* ~52 */ - // U+058A: "֊" ARMENIAN HYPHEN - // U+055C: "՜" ARMENIAN EXCLAMATION MARK - // U+055D: "՝" ARMENIAN COMMA - // U+055E: "՞" ARMENIAN QUESTION MARK - // U+0559: "ՙ" ARMENIAN MODIFIER LETTER LEFT HALF RING - // U+055A: "՚" ARMENIAN APOSTROPHE - // U+055B: "՛" ARMENIAN EMPHASIS MARK - // U+055F: "՟" ARMENIAN ABBREVIATION MARK - /* 53 */ "!fixedColumnOrder!8,!,?,\\,,.,\u058A,\u055C,\u055D,\u055E,:,;,@,\u0559,\u055A,\u055B,\u055F", - /* 54~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, - /* ~99 */ - // U+055C: "՜" ARMENIAN EXCLAMATION MARK - // U+00A1: "¡" INVERTED EXCLAMATION MARK - /* 100 */ "\u055C,\u00A1", - // U+055E: "՞" ARMENIAN QUESTION MARK - // U+00BF: "¿" INVERTED QUESTION MARK - /* 101 */ "\u055E,\u00BF", - }; - - /* Language is: Icelandic */ - private static final String[] LANGUAGE_is = { - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* 0 */ "\u00E1,\u00E4,\u00E6,\u00E5,\u00E0,\u00E2,\u00E3,\u0101", - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - /* 1 */ "\u00E9,\u00EB,\u00E8,\u00EA,\u0119,\u0117,\u0113", - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - /* 2 */ "\u00ED,\u00EF,\u00EE,\u00EC,\u012F,\u012B", - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - /* 3 */ "\u00F3,\u00F6,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D", - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* 4 */ "\u00FA,\u00FC,\u00FB,\u00F9,\u016B", - /* 5~ */ - null, null, null, - /* ~7 */ - // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE - // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS - /* 8 */ "\u00FD,\u00FF", - // U+00F0: "ð" LATIN SMALL LETTER ETH - /* 9 */ "\u00F0", - /* 10 */ null, - // U+00FE: "þ" LATIN SMALL LETTER THORN - /* 11 */ "\u00FE", - /* 12~ */ - null, null, null, null, null, null, null, null, - /* ~19 */ - // U+00F0: "ð" LATIN SMALL LETTER ETH - /* 20 */ "\u00F0", - // U+00E6: "æ" LATIN SMALL LETTER AE - /* 21 */ "\u00E6", - // U+00FE: "þ" LATIN SMALL LETTER THORN - /* 22 */ "\u00FE", - /* 23~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - }; - - /* Language it: Italian */ - private static final String[] LANGUAGE_it = { - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - // U+00AA: "ª" FEMININE ORDINAL INDICATOR - /* 0 */ "\u00E0,\u00E1,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101,\u00AA", - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - /* 1 */ "\u00E8,\u00E9,\u00EA,\u00EB,\u0119,\u0117,\u0113", - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - /* 2 */ "\u00EC,\u00ED,\u00EE,\u00EF,\u012F,\u012B", - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - // U+00BA: "º" MASCULINE ORDINAL INDICATOR - /* 3 */ "\u00F2,\u00F3,\u00F4,\u00F6,\u00F5,\u0153,\u00F8,\u014D,\u00BA", - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* 4 */ "\u00F9,\u00FA,\u00FB,\u00FC,\u016B", - }; - - /* Language iw: Hebrew */ - private static final String[] LANGUAGE_iw = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~44 */ - // Label for "switch to alphabetic" key. - // U+05D0: "א" HEBREW LETTER ALEF - // U+05D1: "ב" HEBREW LETTER BET - // U+05D2: "ג" HEBREW LETTER GIMEL - /* 45 */ "\u05D0\u05D1\u05D2", - // The following characters don't need BIDI mirroring. - // U+2018: "‘" LEFT SINGLE QUOTATION MARK - // U+2019: "’" RIGHT SINGLE QUOTATION MARK - // U+201A: "‚" SINGLE LOW-9 QUOTATION MARK - // U+201C: "“" LEFT DOUBLE QUOTATION MARK - // U+201D: "”" RIGHT DOUBLE QUOTATION MARK - // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK - /* 46 */ "\u2018,\u2019,\u201A", - /* 47 */ "\u201C,\u201D,\u201E", - /* 48 */ "!text/single_laqm_raqm_rtl", - /* 49 */ "!text/double_laqm_raqm_rtl", - /* 50 */ null, - // U+20AA: "₪" NEW SHEQEL SIGN - /* 51 */ "\u20AA", - /* 52 */ null, - /* 53 */ "!fixedColumnOrder!8,;,/,(|),)|(,#,!,\\,,?,&,\\%,+,\",-,:,',@", - // U+2605: "★" BLACK STAR - /* 54 */ "\u2605", - /* 55 */ null, - // U+00B1: "±" PLUS-MINUS SIGN - // U+FB29: "﬩" HEBREW LETTER ALTERNATIVE PLUS SIGN - /* 56 */ "\u00B1,\uFB29", - // The all letters need to be mirrored are found at - // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt - /* 57 */ "!fixedColumnOrder!3,<|>,{|},[|]", - /* 58 */ "!fixedColumnOrder!3,>|<,}|{,]|[", - // U+2264: "≤" LESS-THAN OR EQUAL TO - // U+2265: "≥" GREATER-THAN EQUAL TO - // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK - // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK - // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK - // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK - /* 59 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,\u00AB|\u00BB", - /* 60 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,\u00BB|\u00AB", - /* 61~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~104 */ - /* 105 */ "!", - /* 106 */ "!", - /* 107 */ "?", - /* 108 */ "?", - }; - - /* Language ka: Georgian */ - private static final String[] LANGUAGE_ka = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~44 */ - // Label for "switch to alphabetic" key. - // U+10D0: "ა" GEORGIAN LETTER AN - // U+10D1: "ბ" GEORGIAN LETTER BAN - // U+10D2: "გ" GEORGIAN LETTER GAN - /* 45 */ "\u10D0\u10D1\u10D2", - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - }; - - /* Language kk: Kazakh */ - private static final String[] LANGUAGE_kk = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, - /* ~24 */ - // U+0449: "щ" CYRILLIC SMALL LETTER SHCHA - /* 25 */ "\u0449", - // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN - /* 26 */ "\u044A", - // U+044B: "ы" CYRILLIC SMALL LETTER YERU - /* 27 */ "\u044B", - // U+044D: "э" CYRILLIC SMALL LETTER E - /* 28 */ "\u044D", - // U+0438: "и" CYRILLIC SMALL LETTER I - /* 29 */ "\u0438", - // U+04AF: "ү" CYRILLIC SMALL LETTER STRAIGHT U - // U+04B1: "ұ" CYRILLIC SMALL LETTER STRAIGHT U WITH STROKE - /* 30 */ "\u04AF,\u04B1", - // U+049B: "қ" CYRILLIC SMALL LETTER KA WITH DESCENDER - /* 31 */ "\u049B", - // U+04A3: "ң" CYRILLIC SMALL LETTER EN WITH DESCENDER - /* 32 */ "\u04A3", - // U+0493: "ғ" CYRILLIC SMALL LETTER GHE WITH STROKE - /* 33 */ "\u0493", - // U+0456: "і" CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I - /* 34 */ "\u0456", - // U+04D9: "ә" CYRILLIC SMALL LETTER SCHWA - /* 35 */ "\u04D9", - // U+04E9: "ө" CYRILLIC SMALL LETTER BARRED O - /* 36 */ "\u04E9", - // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN - /* 37 */ "\u044A", - // U+04BB: "һ" CYRILLIC SMALL LETTER SHHA - /* 38 */ "\u04BB", - /* 39~ */ - null, null, null, null, - /* ~42 */ - // U+0451: "ё" CYRILLIC SMALL LETTER IO - /* 43 */ "\u0451", - /* 44 */ null, - // Label for "switch to alphabetic" key. - // U+0410: "А" CYRILLIC CAPITAL LETTER A - // U+0411: "Б" CYRILLIC CAPITAL LETTER BE - // U+0412: "В" CYRILLIC CAPITAL LETTER VE - /* 45 */ "\u0410\u0411\u0412", - }; - - /* Language km: Khmer */ - private static final String[] LANGUAGE_km = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~44 */ - // Label for "switch to alphabetic" key. - // U+1780: "ក" KHMER LETTER KA - // U+1781: "ខ" KHMER LETTER KHA - // U+1782: "គ" KHMER LETTER KO - /* 45 */ "\u1780\u1781\u1782", - /* 46~ */ - null, null, null, null, - /* ~49 */ - // U+17DB: "៛" KHMER CURRENCY SYMBOL RIEL - /* 50 */ "\u17DB,\u00A2,\u00A3,\u20AC,\u00A5,\u20B1", - }; - - /* Language ky: Kirghiz */ - private static final String[] LANGUAGE_ky = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, - /* ~24 */ - // U+0449: "щ" CYRILLIC SMALL LETTER SHCHA - /* 25 */ "\u0449", - // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN - /* 26 */ "\u044A", - // U+044B: "ы" CYRILLIC SMALL LETTER YERU - /* 27 */ "\u044B", - // U+044D: "э" CYRILLIC SMALL LETTER E - /* 28 */ "\u044D", - // U+0438: "и" CYRILLIC SMALL LETTER I - /* 29 */ "\u0438", - // U+04AF: "ү" CYRILLIC SMALL LETTER STRAIGHT U - /* 30 */ "\u04AF", - /* 31 */ null, - // U+04A3: "ң" CYRILLIC SMALL LETTER EN WITH DESCENDER - /* 32 */ "\u04A3", - /* 33~ */ - null, null, null, - /* ~35 */ - // U+04E9: "ө" CYRILLIC SMALL LETTER BARRED O - /* 36 */ "\u04E9", - // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN - /* 37 */ "\u044A", - /* 38~ */ - null, null, null, null, null, - /* ~42 */ - // U+0451: "ё" CYRILLIC SMALL LETTER IO - /* 43 */ "\u0451", - /* 44 */ null, - // Label for "switch to alphabetic" key. - // U+0410: "А" CYRILLIC CAPITAL LETTER A - // U+0411: "Б" CYRILLIC CAPITAL LETTER BE - // U+0412: "В" CYRILLIC CAPITAL LETTER VE - /* 45 */ "\u0410\u0411\u0412", - }; - - /* Language lo: Lao */ - private static final String[] LANGUAGE_lo = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~44 */ - // Label for "switch to alphabetic" key. - // U+0E81: "ກ" LAO LETTER KO - // U+0E82: "ຂ" LAO LETTER KHO SUNG - // U+0E84: "ຄ" LAO LETTER KHO TAM - /* 45 */ "\u0E81\u0E82\u0E84", - /* 46~ */ - null, null, null, null, null, - /* ~50 */ - // U+20AD: "₭" KIP SIGN - /* 51 */ "\u20AD", - }; - - /* Language lt: Lithuanian */ - private static final String[] LANGUAGE_lt = { - // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+00E6: "æ" LATIN SMALL LETTER AE - /* 0 */ "\u0105,\u00E4,\u0101,\u00E0,\u00E1,\u00E2,\u00E3,\u00E5,\u00E6", - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+011B: "ě" LATIN SMALL LETTER E WITH CARON - /* 1 */ "\u0117,\u0119,\u0113,\u00E8,\u00E9,\u00EA,\u00EB,\u011B", - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+0131: "ı" LATIN SMALL LETTER DOTLESS I - /* 2 */ "\u012F,\u012B,\u00EC,\u00ED,\u00EE,\u00EF,\u0131", - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - /* 3 */ "\u00F6,\u00F5,\u00F2,\u00F3,\u00F4,\u0153,\u0151,\u00F8", - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE - // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE - /* 4 */ "\u016B,\u0173,\u00FC,\u016B,\u00F9,\u00FA,\u00FB,\u016F,\u0171", - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE - // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA - /* 5 */ "\u0161,\u00DF,\u015B,\u015F", - // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* 6 */ "\u0146,\u00F1,\u0144,\u0144", - // U+010D: "č" LATIN SMALL LETTER C WITH CARON - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE - /* 7 */ "\u010D,\u00E7,\u0107", - // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE - // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS - /* 8 */ "\u00FD,\u00FF", - // U+010F: "ď" LATIN SMALL LETTER D WITH CARON - /* 9 */ "\u010F", - // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA - // U+0159: "ř" LATIN SMALL LETTER R WITH CARON - // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE - /* 10 */ "\u0157,\u0159,\u0155", - // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA - // U+0165: "ť" LATIN SMALL LETTER T WITH CARON - /* 11 */ "\u0163,\u0165", - // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON - // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE - // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE - /* 12 */ "\u017E,\u017C,\u017A", - // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA - /* 13 */ "\u0137", - // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA - // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE - // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE - // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON - /* 14 */ "\u013C,\u0142,\u013A,\u013E", - // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA - // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE - /* 15 */ "\u0123,\u011F", - /* 16~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - }; - - /* Language lv: Latvian */ - private static final String[] LANGUAGE_lv = { - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK - /* 0 */ "\u0101,\u00E0,\u00E1,\u00E2,\u00E3,\u00E4,\u00E5,\u00E6,\u0105", - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - // U+011B: "ě" LATIN SMALL LETTER E WITH CARON - /* 1 */ "\u0113,\u0117,\u00E8,\u00E9,\u00EA,\u00EB,\u0119,\u011B", - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+0131: "ı" LATIN SMALL LETTER DOTLESS I - /* 2 */ "\u012B,\u012F,\u00EC,\u00ED,\u00EE,\u00EF,\u0131", - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - /* 3 */ "\u00F2,\u00F3,\u00F4,\u00F5,\u00F6,\u0153,\u0151,\u00F8", - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE - // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE - /* 4 */ "\u016B,\u0173,\u00F9,\u00FA,\u00FB,\u00FC,\u016F,\u0171", - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE - // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA - /* 5 */ "\u0161,\u00DF,\u015B,\u015F", - // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* 6 */ "\u0146,\u00F1,\u0144,\u0144", - // U+010D: "č" LATIN SMALL LETTER C WITH CARON - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE - /* 7 */ "\u010D,\u00E7,\u0107", - // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE - // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS - /* 8 */ "\u00FD,\u00FF", - // U+010F: "ď" LATIN SMALL LETTER D WITH CARON - /* 9 */ "\u010F", - // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA - // U+0159: "ř" LATIN SMALL LETTER R WITH CARON - // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE - /* 10 */ "\u0157,\u0159,\u0155", - // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA - // U+0165: "ť" LATIN SMALL LETTER T WITH CARON - /* 11 */ "\u0163,\u0165", - // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON - // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE - // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE - /* 12 */ "\u017E,\u017C,\u017A", - // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA - /* 13 */ "\u0137", - // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA - // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE - // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE - // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON - /* 14 */ "\u013C,\u0142,\u013A,\u013E", - // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA - // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE - /* 15 */ "\u0123,\u011F", - /* 16~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - }; - - /* Language mk: Macedonian */ - private static final String[] LANGUAGE_mk = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, - /* ~38 */ - // U+0455: "ѕ" CYRILLIC SMALL LETTER DZE - /* 39 */ "\u0455", - // U+045C: "ќ" CYRILLIC SMALL LETTER KJE - /* 40 */ "\u045C", - // U+0437: "з" CYRILLIC SMALL LETTER ZE - /* 41 */ "\u0437", - // U+0453: "ѓ" CYRILLIC SMALL LETTER GJE - /* 42 */ "\u0453", - // U+0450: "ѐ" CYRILLIC SMALL LETTER IE WITH GRAVE - /* 43 */ "\u0450", - // U+045D: "ѝ" CYRILLIC SMALL LETTER I WITH GRAVE - /* 44 */ "\u045D", - // Label for "switch to alphabetic" key. - // U+0410: "А" CYRILLIC CAPITAL LETTER A - // U+0411: "Б" CYRILLIC CAPITAL LETTER BE - // U+0412: "В" CYRILLIC CAPITAL LETTER VE - /* 45 */ "\u0410\u0411\u0412", - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - }; - - /* Language mn: Mongolian */ - private static final String[] LANGUAGE_mn = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~44 */ - // Label for "switch to alphabetic" key. - // U+0410: "А" CYRILLIC CAPITAL LETTER A - // U+0411: "Б" CYRILLIC CAPITAL LETTER BE - // U+0412: "В" CYRILLIC CAPITAL LETTER VE - /* 45 */ "\u0410\u0411\u0412", - /* 46~ */ - null, null, null, null, null, - /* ~50 */ - // U+20AE: "₮" TUGRIK SIGN - /* 51 */ "\u20AE", - }; - - /* Language nb: Norwegian Bokmål */ - private static final String[] LANGUAGE_nb = { - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* 0 */ "\u00E0,\u00E4,\u00E1,\u00E2,\u00E3,\u0101", - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - /* 1 */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113", - /* 2 */ null, - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - /* 3 */ "\u00F4,\u00F2,\u00F3,\u00F6,\u00F5,\u0153,\u014D", - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* 4 */ "\u00FC,\u00FB,\u00F9,\u00FA,\u016B", - /* 5~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~19 */ - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - /* 20 */ "\u00E5", - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - /* 21 */ "\u00F8", - // U+00E6: "æ" LATIN SMALL LETTER AE - /* 22 */ "\u00E6", - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - /* 23 */ "\u00F6", - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - /* 24 */ "\u00E4", - /* 25~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_rqm", - /* 47 */ "!text/double_9qm_rqm", - }; - - /* Language ne: Nepali */ - private static final String[] LANGUAGE_ne = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~44 */ - // Label for "switch to alphabetic" key. - // U+0915: "क" DEVANAGARI LETTER KA - // U+0916: "ख" DEVANAGARI LETTER KHA - // U+0917: "ग" DEVANAGARI LETTER GA - /* 45 */ "\u0915\u0916\u0917", - /* 46~ */ - null, null, null, null, null, - /* ~50 */ - // U+0930/U+0941/U+002E "रु." NEPALESE RUPEE SIGN - /* 51 */ "\u0930\u0941.", - /* 52~ */ - null, null, null, null, null, null, null, null, null, null, null, - /* ~62 */ - // U+0967: "१" DEVANAGARI DIGIT ONE - /* 63 */ "\u0967", - // U+0968: "२" DEVANAGARI DIGIT TWO - /* 64 */ "\u0968", - // U+0969: "३" DEVANAGARI DIGIT THREE - /* 65 */ "\u0969", - // U+096A: "४" DEVANAGARI DIGIT FOUR - /* 66 */ "\u096A", - // U+096B: "५" DEVANAGARI DIGIT FIVE - /* 67 */ "\u096B", - // U+096C: "६" DEVANAGARI DIGIT SIX - /* 68 */ "\u096C", - // U+096D: "७" DEVANAGARI DIGIT SEVEN - /* 69 */ "\u096D", - // U+096E: "८" DEVANAGARI DIGIT EIGHT - /* 70 */ "\u096E", - // U+096F: "९" DEVANAGARI DIGIT NINE - /* 71 */ "\u096F", - // U+0966: "०" DEVANAGARI DIGIT ZERO - /* 72 */ "\u0966", - // Label for "switch to symbols" key. - /* 73 */ "?\u0967\u0968\u0969", - // Label for "switch to symbols with microphone" key. This string shouldn't include the "mic" - // part because it'll be appended by the code. - /* 74 */ "\u0967\u0968\u0969", - /* 75 */ "1", - /* 76 */ "2", - /* 77 */ "3", - /* 78 */ "4", - /* 79 */ "5", - /* 80 */ "6", - /* 81 */ "7", - /* 82 */ "8", - /* 83 */ "9", - /* 84 */ "0", - }; - - /* Language nl: Dutch */ - private static final String[] LANGUAGE_nl = { - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* 0 */ "\u00E1,\u00E4,\u00E2,\u00E0,\u00E6,\u00E3,\u00E5,\u0101", - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - /* 1 */ "\u00E9,\u00EB,\u00EA,\u00E8,\u0119,\u0117,\u0113", - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - // U+0133: "ij" LATIN SMALL LIGATURE IJ - /* 2 */ "\u00ED,\u00EF,\u00EC,\u00EE,\u012F,\u012B,\u0133", - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - /* 3 */ "\u00F3,\u00F6,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D", - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* 4 */ "\u00FA,\u00FC,\u00FB,\u00F9,\u016B", - /* 5 */ null, - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* 6 */ "\u00F1,\u0144", - /* 7 */ null, - // U+0133: "ij" LATIN SMALL LIGATURE IJ - /* 8 */ "\u0133", - /* 9~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_rqm", - /* 47 */ "!text/double_9qm_rqm", - }; - - /* Language pl: Polish */ - private static final String[] LANGUAGE_pl = { - // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* 0 */ "\u0105,\u00E1,\u00E0,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101", - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - /* 1 */ "\u0119,\u00E8,\u00E9,\u00EA,\u00EB,\u0117,\u0113", - /* 2 */ null, - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - /* 3 */ "\u00F3,\u00F6,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D", - /* 4 */ null, - // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - /* 5 */ "\u015B,\u00DF,\u0161", - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - /* 6 */ "\u0144,\u00F1", - // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - // U+010D: "č" LATIN SMALL LETTER C WITH CARON - /* 7 */ "\u0107,\u00E7,\u010D", - /* 8~ */ - null, null, null, null, - /* ~11 */ - // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE - // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE - // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON - /* 12 */ "\u017C,\u017A,\u017E", - /* 13 */ null, - // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE - /* 14 */ "\u0142", - /* 15~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, - /* ~45 */ - /* 46 */ "!text/single_9qm_rqm", - /* 47 */ "!text/double_9qm_rqm", - }; - - /* Language pt: Portuguese */ - private static final String[] LANGUAGE_pt = { - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00AA: "ª" FEMININE ORDINAL INDICATOR - /* 0 */ "\u00E1,\u00E3,\u00E0,\u00E2,\u00E4,\u00E5,\u00E6,\u00AA", - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - /* 1 */ "\u00E9,\u00EA,\u00E8,\u0119,\u0117,\u0113,\u00EB", - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - /* 2 */ "\u00ED,\u00EE,\u00EC,\u00EF,\u012F,\u012B", - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - // U+00BA: "º" MASCULINE ORDINAL INDICATOR - /* 3 */ "\u00F3,\u00F5,\u00F4,\u00F2,\u00F6,\u0153,\u00F8,\u014D,\u00BA", - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* 4 */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", - /* 5 */ null, - /* 6 */ null, - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - // U+010D: "č" LATIN SMALL LETTER C WITH CARON - // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE - /* 7 */ "\u00E7,\u010D,\u0107", - }; - - /* Language rm: Raeto-Romance */ - private static final String[] LANGUAGE_rm = { - /* 0~ */ - null, null, null, - /* ~2 */ - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - /* 3 */ "\u00F2,\u00F3,\u00F6,\u00F4,\u00F5,\u0153,\u00F8", - }; - - /* Language ro: Romanian */ - private static final String[] LANGUAGE_ro = { - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+0103: "ă" LATIN SMALL LETTER A WITH BREVE - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* 0 */ "\u00E2,\u00E3,\u0103,\u00E0,\u00E1,\u00E4,\u00E6,\u00E5,\u0101", - /* 1 */ null, - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - /* 2 */ "\u00EE,\u00EF,\u00EC,\u00ED,\u012F,\u012B", - /* 3 */ null, - /* 4 */ null, - // U+0219: "ș" LATIN SMALL LETTER S WITH COMMA BELOW - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - /* 5 */ "\u0219,\u00DF,\u015B,\u0161", - /* 6~ */ - null, null, null, null, null, - /* ~10 */ - // U+021B: "ț" LATIN SMALL LETTER T WITH COMMA BELOW - /* 11 */ "\u021B", - /* 12~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_rqm", - /* 47 */ "!text/double_9qm_rqm", - }; - - /* Language ru: Russian */ - private static final String[] LANGUAGE_ru = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, - /* ~24 */ - // U+0449: "щ" CYRILLIC SMALL LETTER SHCHA - /* 25 */ "\u0449", - // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN - /* 26 */ "\u044A", - // U+044B: "ы" CYRILLIC SMALL LETTER YERU - /* 27 */ "\u044B", - // U+044D: "э" CYRILLIC SMALL LETTER E - /* 28 */ "\u044D", - // U+0438: "и" CYRILLIC SMALL LETTER I - /* 29 */ "\u0438", - /* 30~ */ - null, null, null, null, null, null, null, - /* ~36 */ - // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN - /* 37 */ "\u044A", - /* 38~ */ - null, null, null, null, null, - /* ~42 */ - // U+0451: "ё" CYRILLIC SMALL LETTER IO - /* 43 */ "\u0451", - /* 44 */ null, - // Label for "switch to alphabetic" key. - // U+0410: "А" CYRILLIC CAPITAL LETTER A - // U+0411: "Б" CYRILLIC CAPITAL LETTER BE - // U+0412: "В" CYRILLIC CAPITAL LETTER VE - /* 45 */ "\u0410\u0411\u0412", - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - }; - - /* Language sk: Slovak */ - private static final String[] LANGUAGE_sk = { - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK - /* 0 */ "\u00E1,\u00E4,\u0101,\u00E0,\u00E2,\u00E3,\u00E5,\u00E6,\u0105", - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+011B: "ě" LATIN SMALL LETTER E WITH CARON - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - /* 1 */ "\u00E9,\u011B,\u0113,\u0117,\u00E8,\u00EA,\u00EB,\u0119", - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+0131: "ı" LATIN SMALL LETTER DOTLESS I - /* 2 */ "\u00ED,\u012B,\u012F,\u00EC,\u00EE,\u00EF,\u0131", - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - /* 3 */ "\u00F4,\u00F3,\u00F6,\u00F2,\u00F5,\u0153,\u0151,\u00F8", - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE - /* 4 */ "\u00FA,\u016F,\u00FC,\u016B,\u0173,\u00F9,\u00FB,\u0171", - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE - // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA - /* 5 */ "\u0161,\u00DF,\u015B,\u015F", - // U+0148: "ň" LATIN SMALL LETTER N WITH CARON - // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* 6 */ "\u0148,\u0146,\u00F1,\u0144,\u0144", - // U+010D: "č" LATIN SMALL LETTER C WITH CARON - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE - /* 7 */ "\u010D,\u00E7,\u0107", - // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE - // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS - /* 8 */ "\u00FD,\u00FF", - // U+010F: "ď" LATIN SMALL LETTER D WITH CARON - /* 9 */ "\u010F", - // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE - // U+0159: "ř" LATIN SMALL LETTER R WITH CARON - // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA - /* 10 */ "\u0155,\u0159,\u0157", - // U+0165: "ť" LATIN SMALL LETTER T WITH CARON - // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA - /* 11 */ "\u0165,\u0163", - // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON - // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE - // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE - /* 12 */ "\u017E,\u017C,\u017A", - // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA - /* 13 */ "\u0137", - // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON - // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE - // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA - // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE - /* 14 */ "\u013E,\u013A,\u013C,\u0142", - // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA - // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE - /* 15 */ "\u0123,\u011F", - /* 16~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - /* 48 */ "!text/single_raqm_laqm", - /* 49 */ "!text/double_raqm_laqm", - }; - - /* Language sl: Slovenian */ - private static final String[] LANGUAGE_sl = { - /* 0~ */ - null, null, null, null, null, - /* ~4 */ - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - /* 5 */ "\u0161", - /* 6 */ null, - // U+010D: "č" LATIN SMALL LETTER C WITH CARON - // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE - /* 7 */ "\u010D,\u0107", - /* 8 */ null, - // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE - /* 9 */ "\u0111", - /* 10 */ null, - /* 11 */ null, - // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON - /* 12 */ "\u017E", - /* 13~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - /* 48 */ "!text/single_raqm_laqm", - /* 49 */ "!text/double_raqm_laqm", - }; - - /* Language sr: Serbian */ - private static final String[] LANGUAGE_sr = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, - /* ~38 */ - // TODO: Move these to sr-Latn once we can handle IETF language tag with script name specified. - // BEGIN: More keys definitions for Serbian (Latin) - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE - // <string name="more_keys_for_s">š,ß,ś</string> - // U+010D: "č" LATIN SMALL LETTER C WITH CARON - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE - // <string name="more_keys_for_c">č,ç,ć</string> - // U+010F: "ď" LATIN SMALL LETTER D WITH CARON - // <string name="more_keys_for_d">ď</string> - // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON - // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE - // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE - // <string name="more_keys_for_z">ž,ź,ż</string> - // END: More keys definitions for Serbian (Latin) - // BEGIN: More keys definitions for Serbian (Cyrillic) - // U+0437: "з" CYRILLIC SMALL LETTER ZE - /* 39 */ "\u0437", - // U+045B: "ћ" CYRILLIC SMALL LETTER TSHE - /* 40 */ "\u045B", - // U+0455: "ѕ" CYRILLIC SMALL LETTER DZE - /* 41 */ "\u0455", - // U+0452: "ђ" CYRILLIC SMALL LETTER DJE - /* 42 */ "\u0452", - // U+0450: "ѐ" CYRILLIC SMALL LETTER IE WITH GRAVE - /* 43 */ "\u0450", - // U+045D: "ѝ" CYRILLIC SMALL LETTER I WITH GRAVE - /* 44 */ "\u045D", - // END: More keys definitions for Serbian (Cyrillic) - // Label for "switch to alphabetic" key. - // U+0410: "А" CYRILLIC CAPITAL LETTER A - // U+0411: "Б" CYRILLIC CAPITAL LETTER BE - // U+0412: "В" CYRILLIC CAPITAL LETTER VE - /* 45 */ "\u0410\u0411\u0412", - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - /* 48 */ "!text/single_raqm_laqm", - /* 49 */ "!text/double_raqm_laqm", - }; - - /* Language sv: Swedish */ - private static final String[] LANGUAGE_sv = { - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - /* 0 */ "\u00E1,\u00E0,\u00E2,\u0105,\u00E3", - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - /* 1 */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0119", - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - /* 2 */ "\u00ED,\u00EC,\u00EE,\u00EF", - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - /* 3 */ "\u00F3,\u00F2,\u00F4,\u00F5,\u014D", - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* 4 */ "\u00FC,\u00FA,\u00F9,\u00FB,\u016B", - // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - /* 5 */ "\u015B,\u0161,\u015F,\u00DF", - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0148: "ň" LATIN SMALL LETTER N WITH CARON - /* 6 */ "\u0144,\u00F1,\u0148", - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE - // U+010D: "č" LATIN SMALL LETTER C WITH CARON - /* 7 */ "\u00E7,\u0107,\u010D", - // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE - // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - /* 8 */ "\u00FD,\u00FF,\u00FC", - // U+00F0: "ð" LATIN SMALL LETTER ETH - // U+010F: "ď" LATIN SMALL LETTER D WITH CARON - /* 9 */ "\u00F0,\u010F", - // U+0159: "ř" LATIN SMALL LETTER R WITH CARON - /* 10 */ "\u0159", - // U+0165: "ť" LATIN SMALL LETTER T WITH CARON - // U+00FE: "þ" LATIN SMALL LETTER THORN - /* 11 */ "\u0165,\u00FE", - // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE - // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON - // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE - /* 12 */ "\u017A,\u017E,\u017C", - /* 13 */ null, - // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE - /* 14 */ "\u0142", - /* 15~ */ - null, null, null, null, null, - /* ~19 */ - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - /* 20 */ "\u00E5", - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - /* 21 */ "\u00F6", - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - /* 22 */ "\u00E4", - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+0153: "œ" LATIN SMALL LIGATURE OE - /* 23 */ "\u00F8,\u0153", - // U+00E6: "æ" LATIN SMALL LETTER AE - /* 24 */ "\u00E6", - /* 25~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, - /* ~47 */ - /* 48 */ "!text/single_raqm_laqm", - /* 49 */ "!text/double_raqm_laqm", - }; - - /* Language sw: Swahili */ - private static final String[] LANGUAGE_sw = { - // This is the same as English except more_keys_for_g. - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* 0 */ "\u00E0,\u00E1,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101", - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - /* 1 */ "\u00E8,\u00E9,\u00EA,\u00EB,\u0113", - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - /* 2 */ "\u00EE,\u00EF,\u00ED,\u012B,\u00EC", - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - /* 3 */ "\u00F4,\u00F6,\u00F2,\u00F3,\u0153,\u00F8,\u014D,\u00F5", - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* 4 */ "\u00FB,\u00FC,\u00F9,\u00FA,\u016B", - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - /* 5 */ "\u00DF", - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - /* 6 */ "\u00F1", - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - /* 7 */ "\u00E7", - /* 8~ */ - null, null, null, null, null, null, null, - /* ~14 */ - /* 15 */ "g\'", - }; - - /* Language th: Thai */ - private static final String[] LANGUAGE_th = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~44 */ - // Label for "switch to alphabetic" key. - // U+0E01: "ก" THAI CHARACTER KO KAI - // U+0E02: "ข" THAI CHARACTER KHO KHAI - // U+0E04: "ค" THAI CHARACTER KHO KHWAI - /* 45 */ "\u0E01\u0E02\u0E04", - /* 46~ */ - null, null, null, null, null, - /* ~50 */ - // U+0E3F: "฿" THAI CURRENCY SYMBOL BAHT - /* 51 */ "\u0E3F", - }; - - /* Language tl: Tagalog */ - private static final String[] LANGUAGE_tl = { - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - // U+00AA: "ª" FEMININE ORDINAL INDICATOR - /* 0 */ "\u00E1,\u00E0,\u00E4,\u00E2,\u00E3,\u00E5,\u0105,\u00E6,\u0101,\u00AA", - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - /* 1 */ "\u00E9,\u00E8,\u00EB,\u00EA,\u0119,\u0117,\u0113", - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - /* 2 */ "\u00ED,\u00EF,\u00EC,\u00EE,\u012F,\u012B", - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - // U+00BA: "º" MASCULINE ORDINAL INDICATOR - /* 3 */ "\u00F3,\u00F2,\u00F6,\u00F4,\u00F5,\u00F8,\u0153,\u014D,\u00BA", - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* 4 */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", - /* 5 */ null, - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* 6 */ "\u00F1,\u0144", - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE - // U+010D: "č" LATIN SMALL LETTER C WITH CARON - /* 7 */ "\u00E7,\u0107,\u010D", - }; - - /* Language tr: Turkish */ - private static final String[] LANGUAGE_tr = { - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - /* 0 */ "\u00E2", - /* 1 */ null, - // U+0131: "ı" LATIN SMALL LETTER DOTLESS I - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - /* 2 */ "\u0131,\u00EE,\u00EF,\u00EC,\u00ED,\u012F,\u012B", - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - /* 3 */ "\u00F6,\u00F4,\u0153,\u00F2,\u00F3,\u00F5,\u00F8,\u014D", - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* 4 */ "\u00FC,\u00FB,\u00F9,\u00FA,\u016B", - // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - /* 5 */ "\u015F,\u00DF,\u015B,\u0161", - /* 6 */ null, - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE - // U+010D: "č" LATIN SMALL LETTER C WITH CARON - /* 7 */ "\u00E7,\u0107,\u010D", - /* 8~ */ - null, null, null, null, null, null, null, - /* ~14 */ - // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE - /* 15 */ "\u011F", - }; - - /* Language uk: Ukrainian */ - private static final String[] LANGUAGE_uk = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, - /* ~24 */ - // U+0449: "щ" CYRILLIC SMALL LETTER SHCHA - /* 25 */ "\u0449", - // U+0457: "ї" CYRILLIC SMALL LETTER YI - /* 26 */ "\u0457", - // U+0456: "і" CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I - /* 27 */ "\u0456", - // U+0454: "є" CYRILLIC SMALL LETTER UKRAINIAN IE - /* 28 */ "\u0454", - // U+0438: "и" CYRILLIC SMALL LETTER I - /* 29 */ "\u0438", - /* 30~ */ - null, null, null, - /* ~32 */ - // U+0491: "ґ" CYRILLIC SMALL LETTER GHE WITH UPTURN - /* 33 */ "\u0491", - // U+0457: "ї" CYRILLIC SMALL LETTER YI - /* 34 */ "\u0457", - /* 35 */ null, - /* 36 */ null, - // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN - /* 37 */ "\u044A", - /* 38~ */ - null, null, null, null, null, null, null, - /* ~44 */ - // Label for "switch to alphabetic" key. - // U+0410: "А" CYRILLIC CAPITAL LETTER A - // U+0411: "Б" CYRILLIC CAPITAL LETTER BE - // U+0412: "В" CYRILLIC CAPITAL LETTER VE - /* 45 */ "\u0410\u0411\u0412", - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - /* 48~ */ - null, null, null, - /* ~50 */ - // U+20B4: "₴" HRYVNIA SIGN - /* 51 */ "\u20B4", - }; - - /* Language vi: Vietnamese */ - private static final String[] LANGUAGE_vi = { - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+1EA3: "ả" LATIN SMALL LETTER A WITH HOOK ABOVE - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+1EA1: "ạ" LATIN SMALL LETTER A WITH DOT BELOW - // U+0103: "ă" LATIN SMALL LETTER A WITH BREVE - // U+1EB1: "ằ" LATIN SMALL LETTER A WITH BREVE AND GRAVE - // U+1EAF: "ắ" LATIN SMALL LETTER A WITH BREVE AND ACUTE - // U+1EB3: "ẳ" LATIN SMALL LETTER A WITH BREVE AND HOOK ABOVE - // U+1EB5: "ẵ" LATIN SMALL LETTER A WITH BREVE AND TILDE - // U+1EB7: "ặ" LATIN SMALL LETTER A WITH BREVE AND DOT BELOW - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+1EA7: "ầ" LATIN SMALL LETTER A WITH CIRCUMFLEX AND GRAVE - // U+1EA5: "ấ" LATIN SMALL LETTER A WITH CIRCUMFLEX AND ACUTE - // U+1EA9: "ẩ" LATIN SMALL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE - // U+1EAB: "ẫ" LATIN SMALL LETTER A WITH CIRCUMFLEX AND TILDE - // U+1EAD: "ậ" LATIN SMALL LETTER A WITH CIRCUMFLEX AND DOT BELOW - /* 0 */ "\u00E0,\u00E1,\u1EA3,\u00E3,\u1EA1,\u0103,\u1EB1,\u1EAF,\u1EB3,\u1EB5,\u1EB7,\u00E2,\u1EA7,\u1EA5,\u1EA9,\u1EAB,\u1EAD", - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+1EBB: "ẻ" LATIN SMALL LETTER E WITH HOOK ABOVE - // U+1EBD: "ẽ" LATIN SMALL LETTER E WITH TILDE - // U+1EB9: "ẹ" LATIN SMALL LETTER E WITH DOT BELOW - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+1EC1: "ề" LATIN SMALL LETTER E WITH CIRCUMFLEX AND GRAVE - // U+1EBF: "ế" LATIN SMALL LETTER E WITH CIRCUMFLEX AND ACUTE - // U+1EC3: "ể" LATIN SMALL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE - // U+1EC5: "ễ" LATIN SMALL LETTER E WITH CIRCUMFLEX AND TILDE - // U+1EC7: "ệ" LATIN SMALL LETTER E WITH CIRCUMFLEX AND DOT BELOW - /* 1 */ "\u00E8,\u00E9,\u1EBB,\u1EBD,\u1EB9,\u00EA,\u1EC1,\u1EBF,\u1EC3,\u1EC5,\u1EC7", - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+1EC9: "ỉ" LATIN SMALL LETTER I WITH HOOK ABOVE - // U+0129: "ĩ" LATIN SMALL LETTER I WITH TILDE - // U+1ECB: "ị" LATIN SMALL LETTER I WITH DOT BELOW - /* 2 */ "\u00EC,\u00ED,\u1EC9,\u0129,\u1ECB", - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+1ECF: "ỏ" LATIN SMALL LETTER O WITH HOOK ABOVE - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+1ECD: "ọ" LATIN SMALL LETTER O WITH DOT BELOW - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+1ED3: "ồ" LATIN SMALL LETTER O WITH CIRCUMFLEX AND GRAVE - // U+1ED1: "ố" LATIN SMALL LETTER O WITH CIRCUMFLEX AND ACUTE - // U+1ED5: "ổ" LATIN SMALL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE - // U+1ED7: "ỗ" LATIN SMALL LETTER O WITH CIRCUMFLEX AND TILDE - // U+1ED9: "ộ" LATIN SMALL LETTER O WITH CIRCUMFLEX AND DOT BELOW - // U+01A1: "ơ" LATIN SMALL LETTER O WITH HORN - // U+1EDD: "ờ" LATIN SMALL LETTER O WITH HORN AND GRAVE - // U+1EDB: "ớ" LATIN SMALL LETTER O WITH HORN AND ACUTE - // U+1EDF: "ở" LATIN SMALL LETTER O WITH HORN AND HOOK ABOVE - // U+1EE1: "ỡ" LATIN SMALL LETTER O WITH HORN AND TILDE - // U+1EE3: "ợ" LATIN SMALL LETTER O WITH HORN AND DOT BELOW - /* 3 */ "\u00F2,\u00F3,\u1ECF,\u00F5,\u1ECD,\u00F4,\u1ED3,\u1ED1,\u1ED5,\u1ED7,\u1ED9,\u01A1,\u1EDD,\u1EDB,\u1EDF,\u1EE1,\u1EE3", - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+1EE7: "ủ" LATIN SMALL LETTER U WITH HOOK ABOVE - // U+0169: "ũ" LATIN SMALL LETTER U WITH TILDE - // U+1EE5: "ụ" LATIN SMALL LETTER U WITH DOT BELOW - // U+01B0: "ư" LATIN SMALL LETTER U WITH HORN - // U+1EEB: "ừ" LATIN SMALL LETTER U WITH HORN AND GRAVE - // U+1EE9: "ứ" LATIN SMALL LETTER U WITH HORN AND ACUTE - // U+1EED: "ử" LATIN SMALL LETTER U WITH HORN AND HOOK ABOVE - // U+1EEF: "ữ" LATIN SMALL LETTER U WITH HORN AND TILDE - // U+1EF1: "ự" LATIN SMALL LETTER U WITH HORN AND DOT BELOW - /* 4 */ "\u00F9,\u00FA,\u1EE7,\u0169,\u1EE5,\u01B0,\u1EEB,\u1EE9,\u1EED,\u1EEF,\u1EF1", - /* 5~ */ - null, null, null, - /* ~7 */ - // U+1EF3: "ỳ" LATIN SMALL LETTER Y WITH GRAVE - // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE - // U+1EF7: "ỷ" LATIN SMALL LETTER Y WITH HOOK ABOVE - // U+1EF9: "ỹ" LATIN SMALL LETTER Y WITH TILDE - // U+1EF5: "ỵ" LATIN SMALL LETTER Y WITH DOT BELOW - /* 8 */ "\u1EF3,\u00FD,\u1EF7,\u1EF9,\u1EF5", - // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE - /* 9 */ "\u0111", - /* 10~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, - /* ~50 */ - // U+20AB: "₫" DONG SIGN - /* 51 */ "\u20AB", - }; - - /* Language zu: Zulu */ - private static final String[] LANGUAGE_zu = { - // This is the same as English - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* 0 */ "\u00E0,\u00E1,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101", - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - /* 1 */ "\u00E8,\u00E9,\u00EA,\u00EB,\u0113", - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - /* 2 */ "\u00EE,\u00EF,\u00ED,\u012B,\u00EC", - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - /* 3 */ "\u00F4,\u00F6,\u00F2,\u00F3,\u0153,\u00F8,\u014D,\u00F5", - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* 4 */ "\u00FB,\u00FC,\u00F9,\u00FA,\u016B", - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - /* 5 */ "\u00DF", - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - /* 6 */ "\u00F1", - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - /* 7 */ "\u00E7", - }; - - /* Language zz: Alphabet */ - private static final String[] LANGUAGE_zz = { - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - // U+0103: "ă" LATIN SMALL LETTER A WITH BREVE - // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK - // U+00AA: "ª" FEMININE ORDINAL INDICATOR - /* 0 */ "\u00E0,\u00E1,\u00E2,\u00E3,\u00E4,\u00E5,\u00E6,\u00E3,\u00E5,\u0101,\u0103,\u0105,\u00AA", - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - // U+0115: "ĕ" LATIN SMALL LETTER E WITH BREVE - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - // U+011B: "ě" LATIN SMALL LETTER E WITH CARON - /* 1 */ "\u00E8,\u00E9,\u00EA,\u00EB,\u0113,\u0115,\u0117,\u0119,\u011B", - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+0129: "ĩ" LATIN SMALL LETTER I WITH TILDE - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - // U+012D: "ĭ" LATIN SMALL LETTER I WITH BREVE - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+0131: "ı" LATIN SMALL LETTER DOTLESS I - // U+0133: "ij" LATIN SMALL LIGATURE IJ - /* 2 */ "\u00EC,\u00ED,\u00EE,\u00EF,\u0129,\u012B,\u012D,\u012F,\u0131,\u0133", - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - // U+014F: "ŏ" LATIN SMALL LETTER O WITH BREVE - // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00BA: "º" MASCULINE ORDINAL INDICATOR - /* 3 */ "\u00F2,\u00F3,\u00F4,\u00F5,\u00F6,\u00F8,\u014D,\u014F,\u0151,\u0153,\u00BA", - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+0169: "ũ" LATIN SMALL LETTER U WITH TILDE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - // U+016D: "ŭ" LATIN SMALL LETTER U WITH BREVE - // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE - // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE - // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK - /* 4 */ "\u00F9,\u00FA,\u00FB,\u00FC,\u0169,\u016B,\u016D,\u016F,\u0171,\u0173", - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE - // U+015D: "ŝ" LATIN SMALL LETTER S WITH CIRCUMFLEX - // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - // U+017F: "ſ" LATIN SMALL LETTER LONG S - /* 5 */ "\u00DF,\u015B,\u015D,\u015F,\u0161,\u017F", - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA - // U+0148: "ň" LATIN SMALL LETTER N WITH CARON - // U+0149: "ʼn" LATIN SMALL LETTER N PRECEDED BY APOSTROPHE - // U+014B: "ŋ" LATIN SMALL LETTER ENG - /* 6 */ "\u00F1,\u0144,\u0146,\u0148,\u0149,\u014B", - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE - // U+0109: "ĉ" LATIN SMALL LETTER C WITH CIRCUMFLEX - // U+010B: "ċ" LATIN SMALL LETTER C WITH DOT ABOVE - // U+010D: "č" LATIN SMALL LETTER C WITH CARON - /* 7 */ "\u00E7,\u0107,\u0109,\u010B,\u010D", - // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE - // U+0177: "ŷ" LATIN SMALL LETTER Y WITH CIRCUMFLEX - // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS - // U+0133: "ij" LATIN SMALL LIGATURE IJ - /* 8 */ "\u00FD,\u0177,\u00FF,\u0133", - // U+010F: "ď" LATIN SMALL LETTER D WITH CARON - // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE - // U+00F0: "ð" LATIN SMALL LETTER ETH - /* 9 */ "\u010F,\u0111,\u00F0", - // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE - // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA - // U+0159: "ř" LATIN SMALL LETTER R WITH CARON - /* 10 */ "\u0155,\u0157,\u0159", - // U+00FE: "þ" LATIN SMALL LETTER THORN - // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA - // U+0165: "ť" LATIN SMALL LETTER T WITH CARON - // U+0167: "ŧ" LATIN SMALL LETTER T WITH STROKE - /* 11 */ "\u00FE,\u0163,\u0165,\u0167", - // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE - // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE - // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON - /* 12 */ "\u017A,\u017C,\u017E", - // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA - // U+0138: "ĸ" LATIN SMALL LETTER KRA - /* 13 */ "\u0137,\u0138", - // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE - // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA - // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON - // U+0140: "ŀ" LATIN SMALL LETTER L WITH MIDDLE DOT - // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE - /* 14 */ "\u013A,\u013C,\u013E,\u0140,\u0142", - // U+011D: "ĝ" LATIN SMALL LETTER G WITH CIRCUMFLEX - // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE - // U+0121: "ġ" LATIN SMALL LETTER G WITH DOT ABOVE - // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA - /* 15 */ "\u011D,\u011F,\u0121,\u0123", - /* 16 */ null, - // U+0125: "ĥ" LATIN SMALL LETTER H WITH CIRCUMFLEX - /* 17 */ "\u0125", - // U+0135: "ĵ" LATIN SMALL LETTER J WITH CIRCUMFLEX - /* 18 */ "\u0135", - // U+0175: "ŵ" LATIN SMALL LETTER W WITH CIRCUMFLEX - /* 19 */ "\u0175", - }; - - private static final Object[] LANGUAGES_AND_TEXTS = { - "DEFAULT", LANGUAGE_DEFAULT, /* default */ - "af", LANGUAGE_af, /* Afrikaans */ - "ar", LANGUAGE_ar, /* Arabic */ - "az", LANGUAGE_az, /* Azerbaijani */ - "be", LANGUAGE_be, /* Belarusian */ - "bg", LANGUAGE_bg, /* Bulgarian */ - "ca", LANGUAGE_ca, /* Catalan */ - "cs", LANGUAGE_cs, /* Czech */ - "da", LANGUAGE_da, /* Danish */ - "de", LANGUAGE_de, /* German */ - "el", LANGUAGE_el, /* Greek */ - "en", LANGUAGE_en, /* English */ - "eo", LANGUAGE_eo, /* Esperanto */ - "es", LANGUAGE_es, /* Spanish */ - "et", LANGUAGE_et, /* Estonian */ - "fa", LANGUAGE_fa, /* Persian */ - "fi", LANGUAGE_fi, /* Finnish */ - "fr", LANGUAGE_fr, /* French */ - "hi", LANGUAGE_hi, /* Hindi */ - "hr", LANGUAGE_hr, /* Croatian */ - "hu", LANGUAGE_hu, /* Hungarian */ - "hy", LANGUAGE_hy, /* Armenian */ - "is", LANGUAGE_is, /* Icelandic */ - "it", LANGUAGE_it, /* Italian */ - "iw", LANGUAGE_iw, /* Hebrew */ - "ka", LANGUAGE_ka, /* Georgian */ - "kk", LANGUAGE_kk, /* Kazakh */ - "km", LANGUAGE_km, /* Khmer */ - "ky", LANGUAGE_ky, /* Kirghiz */ - "lo", LANGUAGE_lo, /* Lao */ - "lt", LANGUAGE_lt, /* Lithuanian */ - "lv", LANGUAGE_lv, /* Latvian */ - "mk", LANGUAGE_mk, /* Macedonian */ - "mn", LANGUAGE_mn, /* Mongolian */ - "nb", LANGUAGE_nb, /* Norwegian Bokmål */ - "ne", LANGUAGE_ne, /* Nepali */ - "nl", LANGUAGE_nl, /* Dutch */ - "pl", LANGUAGE_pl, /* Polish */ - "pt", LANGUAGE_pt, /* Portuguese */ - "rm", LANGUAGE_rm, /* Raeto-Romance */ - "ro", LANGUAGE_ro, /* Romanian */ - "ru", LANGUAGE_ru, /* Russian */ - "sk", LANGUAGE_sk, /* Slovak */ - "sl", LANGUAGE_sl, /* Slovenian */ - "sr", LANGUAGE_sr, /* Serbian */ - "sv", LANGUAGE_sv, /* Swedish */ - "sw", LANGUAGE_sw, /* Swahili */ - "th", LANGUAGE_th, /* Thai */ - "tl", LANGUAGE_tl, /* Tagalog */ - "tr", LANGUAGE_tr, /* Turkish */ - "uk", LANGUAGE_uk, /* Ukrainian */ - "vi", LANGUAGE_vi, /* Vietnamese */ - "zu", LANGUAGE_zu, /* Zulu */ - "zz", LANGUAGE_zz, /* Alphabet */ - }; - - static { - int id = 0; - for (final String name : NAMES) { - sNameToIdsMap.put(name, id++); - } - - for (int i = 0; i < LANGUAGES_AND_TEXTS.length; i += 2) { - final String language = (String)LANGUAGES_AND_TEXTS[i]; - final String[] texts = (String[])LANGUAGES_AND_TEXTS[i + 1]; - sLocaleToTextsMap.put(language, texts); - } - } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsTable.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsTable.java new file mode 100644 index 000000000..6afc6210c --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsTable.java @@ -0,0 +1,3892 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.internal; + +import java.util.HashMap; +import java.util.Locale; + +/** + * !!!!! DO NOT EDIT THIS FILE !!!!! + * + * This file is generated by tools/make-keyboard-text. The base template file is + * tools/make-keyboard-text/res/src/com/android/inputmethod/keyboard/internal/ + * KeyboardTextsTable.tmpl + * + * This file must be updated when any text resources in keyboard layout files have been changed. + * These text resources are referred as "!text/<resource_name>" in keyboard XML definitions, + * and should be defined in + * tools/make-keyboard-text/res/values-<locale>/donottranslate-more-keys.xml + * + * To update this file, please run the following commands. + * $ cd $ANDROID_BUILD_TOP + * $ mmm packages/inputmethods/LatinIME/tools/make-keyboard-text + * $ make-keyboard-text -java packages/inputmethods/LatinIME/java + * + * The updated source file will be generated to the following path (this file). + * packages/inputmethods/LatinIME/java/src/com/android/inputmethod/keyboard/internal/ + * KeyboardTextsTable.java + */ +public final class KeyboardTextsTable { + // Name to index map. + private static final HashMap<String, Integer> sNameToIndexesMap = new HashMap<>(); + // Locale to texts table map. + private static final HashMap<String, String[]> sLocaleToTextsTableMap = new HashMap<>(); + // TODO: Remove this variable after debugging. + // Texts table to locale maps. + private static final HashMap<String[], String> sTextsTableToLocaleMap = new HashMap<>(); + + public static String getText(final String name, final String[] textsTable) { + final Integer indexObj = sNameToIndexesMap.get(name); + if (indexObj == null) { + throw new RuntimeException("Unknown text name=" + name + " locale=" + + sTextsTableToLocaleMap.get(textsTable)); + } + final int index = indexObj; + final String text = (index < textsTable.length) ? textsTable[index] : null; + if (text != null) { + return text; + } + // Sanity check. + if (index >= 0 && index < TEXTS_DEFAULT.length) { + return TEXTS_DEFAULT[index]; + } + // Throw exception for debugging purpose. + throw new RuntimeException("Illegal index=" + index + " for name=" + name + + " locale=" + sTextsTableToLocaleMap.get(textsTable)); + } + + public static String[] getTextsTable(final Locale locale) { + final String localeKey = locale.toString(); + if (sLocaleToTextsTableMap.containsKey(localeKey)) { + return sLocaleToTextsTableMap.get(localeKey); + } + final String languageKey = locale.getLanguage(); + if (sLocaleToTextsTableMap.containsKey(languageKey)) { + return sLocaleToTextsTableMap.get(languageKey); + } + return TEXTS_DEFAULT; + } + + private static final String[] NAMES = { + // /* index:histogram */ "name", + /* 0:32 */ "morekeys_a", + /* 1:32 */ "morekeys_o", + /* 2:30 */ "morekeys_u", + /* 3:29 */ "morekeys_e", + /* 4:28 */ "morekeys_i", + /* 5:24 */ "keylabel_to_alpha", + /* 6:23 */ "morekeys_c", + /* 7:23 */ "double_quotes", + /* 8:22 */ "morekeys_n", + /* 9:22 */ "single_quotes", + /* 10:20 */ "morekeys_s", + /* 11:14 */ "morekeys_y", + /* 12:13 */ "morekeys_d", + /* 13:12 */ "morekeys_z", + /* 14:11 */ "keyspec_currency", + /* 15:10 */ "morekeys_t", + /* 16:10 */ "morekeys_l", + /* 17: 9 */ "morekeys_g", + /* 18: 9 */ "single_angle_quotes", + /* 19: 9 */ "double_angle_quotes", + /* 20: 8 */ "morekeys_r", + /* 21: 6 */ "morekeys_k", + /* 22: 6 */ "morekeys_cyrillic_ie", + /* 23: 5 */ "keyspec_nordic_row1_11", + /* 24: 5 */ "keyspec_nordic_row2_10", + /* 25: 5 */ "keyspec_nordic_row2_11", + /* 26: 5 */ "morekeys_nordic_row2_10", + /* 27: 5 */ "keyspec_east_slavic_row1_9", + /* 28: 5 */ "keyspec_east_slavic_row2_2", + /* 29: 5 */ "keyspec_east_slavic_row2_11", + /* 30: 5 */ "keyspec_east_slavic_row3_5", + /* 31: 5 */ "morekeys_cyrillic_soft_sign", + /* 32: 5 */ "keyspec_symbols_1", + /* 33: 5 */ "keyspec_symbols_2", + /* 34: 5 */ "keyspec_symbols_3", + /* 35: 5 */ "keyspec_symbols_4", + /* 36: 5 */ "keyspec_symbols_5", + /* 37: 5 */ "keyspec_symbols_6", + /* 38: 5 */ "keyspec_symbols_7", + /* 39: 5 */ "keyspec_symbols_8", + /* 40: 5 */ "keyspec_symbols_9", + /* 41: 5 */ "keyspec_symbols_0", + /* 42: 5 */ "keylabel_to_symbol", + /* 43: 5 */ "additional_morekeys_symbols_1", + /* 44: 5 */ "additional_morekeys_symbols_2", + /* 45: 5 */ "additional_morekeys_symbols_3", + /* 46: 5 */ "additional_morekeys_symbols_4", + /* 47: 5 */ "additional_morekeys_symbols_5", + /* 48: 5 */ "additional_morekeys_symbols_6", + /* 49: 5 */ "additional_morekeys_symbols_7", + /* 50: 5 */ "additional_morekeys_symbols_8", + /* 51: 5 */ "additional_morekeys_symbols_9", + /* 52: 5 */ "additional_morekeys_symbols_0", + /* 53: 4 */ "morekeys_nordic_row2_11", + /* 54: 4 */ "morekeys_punctuation", + /* 55: 4 */ "keyspec_tablet_comma", + /* 56: 3 */ "keyspec_swiss_row1_11", + /* 57: 3 */ "keyspec_swiss_row2_10", + /* 58: 3 */ "keyspec_swiss_row2_11", + /* 59: 3 */ "morekeys_swiss_row1_11", + /* 60: 3 */ "morekeys_swiss_row2_10", + /* 61: 3 */ "morekeys_swiss_row2_11", + /* 62: 3 */ "morekeys_star", + /* 63: 3 */ "keyspec_left_parenthesis", + /* 64: 3 */ "keyspec_right_parenthesis", + /* 65: 3 */ "keyspec_left_square_bracket", + /* 66: 3 */ "keyspec_right_square_bracket", + /* 67: 3 */ "keyspec_left_curly_bracket", + /* 68: 3 */ "keyspec_right_curly_bracket", + /* 69: 3 */ "keyspec_less_than", + /* 70: 3 */ "keyspec_greater_than", + /* 71: 3 */ "keyspec_less_than_equal", + /* 72: 3 */ "keyspec_greater_than_equal", + /* 73: 3 */ "keyspec_left_double_angle_quote", + /* 74: 3 */ "keyspec_right_double_angle_quote", + /* 75: 3 */ "keyspec_left_single_angle_quote", + /* 76: 3 */ "keyspec_right_single_angle_quote", + /* 77: 3 */ "morekeys_tablet_comma", + /* 78: 3 */ "keyhintlabel_period", + /* 79: 3 */ "morekeys_tablet_period", + /* 80: 3 */ "morekeys_question", + /* 81: 2 */ "morekeys_h", + /* 82: 2 */ "morekeys_w", + /* 83: 2 */ "morekeys_east_slavic_row2_2", + /* 84: 2 */ "morekeys_cyrillic_u", + /* 85: 2 */ "morekeys_cyrillic_en", + /* 86: 2 */ "morekeys_cyrillic_ghe", + /* 87: 2 */ "morekeys_cyrillic_o", + /* 88: 2 */ "morekeys_cyrillic_i", + /* 89: 2 */ "keyspec_south_slavic_row1_6", + /* 90: 2 */ "keyspec_south_slavic_row2_11", + /* 91: 2 */ "keyspec_south_slavic_row3_1", + /* 92: 2 */ "keyspec_south_slavic_row3_8", + /* 93: 2 */ "morekeys_tablet_punctuation", + /* 94: 2 */ "keyspec_spanish_row2_10", + /* 95: 2 */ "morekeys_bullet", + /* 96: 2 */ "morekeys_left_parenthesis", + /* 97: 2 */ "morekeys_right_parenthesis", + /* 98: 2 */ "morekeys_arabic_diacritics", + /* 99: 2 */ "keyspec_comma", + /* 100: 2 */ "keyhintlabel_tablet_comma", + /* 101: 2 */ "keyspec_period", + /* 102: 2 */ "morekeys_period", + /* 103: 2 */ "keyspec_tablet_period", + /* 104: 2 */ "keyhintlabel_tablet_period", + /* 105: 2 */ "keyspec_symbols_question", + /* 106: 2 */ "keyspec_symbols_semicolon", + /* 107: 2 */ "keyspec_symbols_percent", + /* 108: 2 */ "morekeys_symbols_semicolon", + /* 109: 2 */ "morekeys_symbols_percent", + /* 110: 1 */ "morekeys_v", + /* 111: 1 */ "morekeys_j", + /* 112: 1 */ "morekeys_q", + /* 113: 1 */ "morekeys_x", + /* 114: 1 */ "keyspec_q", + /* 115: 1 */ "keyspec_w", + /* 116: 1 */ "keyspec_y", + /* 117: 1 */ "keyspec_x", + /* 118: 1 */ "morekeys_east_slavic_row2_11", + /* 119: 1 */ "morekeys_cyrillic_ka", + /* 120: 1 */ "morekeys_cyrillic_a", + /* 121: 1 */ "morekeys_currency_dollar", + /* 122: 1 */ "morekeys_plus", + /* 123: 1 */ "morekeys_less_than", + /* 124: 1 */ "morekeys_greater_than", + /* 125: 1 */ "morekeys_exclamation", + /* 126: 0 */ "morekeys_currency", + /* 127: 0 */ "morekeys_symbols_1", + /* 128: 0 */ "morekeys_symbols_2", + /* 129: 0 */ "morekeys_symbols_3", + /* 130: 0 */ "morekeys_symbols_4", + /* 131: 0 */ "morekeys_symbols_5", + /* 132: 0 */ "morekeys_symbols_6", + /* 133: 0 */ "morekeys_symbols_7", + /* 134: 0 */ "morekeys_symbols_8", + /* 135: 0 */ "morekeys_symbols_9", + /* 136: 0 */ "morekeys_symbols_0", + /* 137: 0 */ "morekeys_am_pm", + /* 138: 0 */ "keyspec_settings", + /* 139: 0 */ "keyspec_shortcut", + /* 140: 0 */ "keyspec_action_next", + /* 141: 0 */ "keyspec_action_previous", + /* 142: 0 */ "keylabel_to_more_symbol", + /* 143: 0 */ "keylabel_tablet_to_more_symbol", + /* 144: 0 */ "keylabel_to_phone_numeric", + /* 145: 0 */ "keylabel_to_phone_symbols", + /* 146: 0 */ "keylabel_time_am", + /* 147: 0 */ "keylabel_time_pm", + /* 148: 0 */ "keyspec_popular_domain", + /* 149: 0 */ "morekeys_popular_domain", + /* 150: 0 */ "keyspecs_left_parenthesis_more_keys", + /* 151: 0 */ "keyspecs_right_parenthesis_more_keys", + /* 152: 0 */ "single_laqm_raqm", + /* 153: 0 */ "single_raqm_laqm", + /* 154: 0 */ "double_laqm_raqm", + /* 155: 0 */ "double_raqm_laqm", + /* 156: 0 */ "single_lqm_rqm", + /* 157: 0 */ "single_9qm_lqm", + /* 158: 0 */ "single_9qm_rqm", + /* 159: 0 */ "single_rqm_9qm", + /* 160: 0 */ "double_lqm_rqm", + /* 161: 0 */ "double_9qm_lqm", + /* 162: 0 */ "double_9qm_rqm", + /* 163: 0 */ "double_rqm_9qm", + /* 164: 0 */ "morekeys_single_quote", + /* 165: 0 */ "morekeys_double_quote", + /* 166: 0 */ "morekeys_tablet_double_quote", + /* 167: 0 */ "keyspec_emoji_key", + }; + + private static final String EMPTY = ""; + + /* Default texts */ + private static final String[] TEXTS_DEFAULT = { + /* morekeys_a ~ */ + EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, + /* ~ morekeys_i */ + // Label for "switch to alphabetic" key. + /* keylabel_to_alpha */ "ABC", + /* morekeys_c */ EMPTY, + /* double_quotes */ "!text/double_lqm_rqm", + /* morekeys_n */ EMPTY, + /* single_quotes */ "!text/single_lqm_rqm", + /* morekeys_s ~ */ + EMPTY, EMPTY, EMPTY, EMPTY, + /* ~ morekeys_z */ + /* keyspec_currency */ "$", + /* morekeys_t ~ */ + EMPTY, EMPTY, EMPTY, + /* ~ morekeys_g */ + /* single_angle_quotes */ "!text/single_laqm_raqm", + /* double_angle_quotes */ "!text/double_laqm_raqm", + /* morekeys_r ~ */ + EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, + /* ~ morekeys_cyrillic_soft_sign */ + /* keyspec_symbols_1 */ "1", + /* keyspec_symbols_2 */ "2", + /* keyspec_symbols_3 */ "3", + /* keyspec_symbols_4 */ "4", + /* keyspec_symbols_5 */ "5", + /* keyspec_symbols_6 */ "6", + /* keyspec_symbols_7 */ "7", + /* keyspec_symbols_8 */ "8", + /* keyspec_symbols_9 */ "9", + /* keyspec_symbols_0 */ "0", + // Label for "switch to symbols" key. + /* keylabel_to_symbol */ "?123", + /* additional_morekeys_symbols_1 ~ */ + EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, + /* ~ morekeys_nordic_row2_11 */ + /* morekeys_punctuation */ "!autoColumnOrder!8,\\,,?,!,#,!text/keyspec_right_parenthesis,!text/keyspec_left_parenthesis,/,;,',@,:,-,\",+,\\%,&", + /* keyspec_tablet_comma */ ",", + /* keyspec_swiss_row1_11 ~ */ + EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, + /* ~ morekeys_swiss_row2_11 */ + // U+2020: "†" DAGGER + // U+2021: "‡" DOUBLE DAGGER + // U+2605: "★" BLACK STAR + /* morekeys_star */ "\u2020,\u2021,\u2605", + // The all letters need to be mirrored are found at + // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt + // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK + // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + // U+2264: "≤" LESS-THAN OR EQUAL TO + // U+2265: "≥" GREATER-THAN EQUAL TO + // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + /* keyspec_left_parenthesis */ "(", + /* keyspec_right_parenthesis */ ")", + /* keyspec_left_square_bracket */ "[", + /* keyspec_right_square_bracket */ "]", + /* keyspec_left_curly_bracket */ "{", + /* keyspec_right_curly_bracket */ "}", + /* keyspec_less_than */ "<", + /* keyspec_greater_than */ ">", + /* keyspec_less_than_equal */ "\u2264", + /* keyspec_greater_than_equal */ "\u2265", + /* keyspec_left_double_angle_quote */ "\u00AB", + /* keyspec_right_double_angle_quote */ "\u00BB", + /* keyspec_left_single_angle_quote */ "\u2039", + /* keyspec_right_single_angle_quote */ "\u203A", + /* morekeys_tablet_comma */ EMPTY, + /* keyhintlabel_period */ EMPTY, + /* morekeys_tablet_period */ "!text/morekeys_tablet_punctuation", + // U+00BF: "¿" INVERTED QUESTION MARK + /* morekeys_question */ "\u00BF", + /* morekeys_h ~ */ + EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, + /* ~ keyspec_south_slavic_row3_8 */ + /* morekeys_tablet_punctuation */ "!autoColumnOrder!7,\\,,',#,!text/keyspec_right_parenthesis,!text/keyspec_left_parenthesis,/,;,@,:,-,\",+,\\%,&", + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + /* keyspec_spanish_row2_10 */ "\u00F1", + // U+266A: "♪" EIGHTH NOTE + // U+2665: "♥" BLACK HEART SUIT + // U+2660: "♠" BLACK SPADE SUIT + // U+2666: "♦" BLACK DIAMOND SUIT + // U+2663: "♣" BLACK CLUB SUIT + /* morekeys_bullet */ "\u266A,\u2665,\u2660,\u2666,\u2663", + /* morekeys_left_parenthesis */ "!fixedColumnOrder!3,!text/keyspecs_left_parenthesis_more_keys", + /* morekeys_right_parenthesis */ "!fixedColumnOrder!3,!text/keyspecs_right_parenthesis_more_keys", + /* morekeys_arabic_diacritics */ EMPTY, + // Comma key + /* keyspec_comma */ ",", + /* keyhintlabel_tablet_comma */ EMPTY, + // Period key + /* keyspec_period */ ".", + /* morekeys_period */ "!text/morekeys_punctuation", + /* keyspec_tablet_period */ ".", + /* keyhintlabel_tablet_period */ EMPTY, + /* keyspec_symbols_question */ "?", + /* keyspec_symbols_semicolon */ ";", + /* keyspec_symbols_percent */ "%", + /* morekeys_symbols_semicolon */ EMPTY, + // U+2030: "‰" PER MILLE SIGN + /* morekeys_symbols_percent */ "\u2030", + /* morekeys_v ~ */ + EMPTY, EMPTY, EMPTY, EMPTY, + /* ~ morekeys_x */ + /* keyspec_q */ "q", + /* keyspec_w */ "w", + /* keyspec_y */ "y", + /* keyspec_x */ "x", + /* morekeys_east_slavic_row2_11 ~ */ + EMPTY, EMPTY, EMPTY, + /* ~ morekeys_cyrillic_a */ + // U+00A2: "¢" CENT SIGN + // U+00A3: "£" POUND SIGN + // U+20AC: "€" EURO SIGN + // U+00A5: "¥" YEN SIGN + // U+20B1: "₱" PESO SIGN + /* morekeys_currency_dollar */ "\u00A2,\u00A3,\u20AC,\u00A5,\u20B1", + // U+00B1: "±" PLUS-MINUS SIGN + /* morekeys_plus */ "\u00B1", + /* morekeys_less_than */ "!fixedColumnOrder!3,!text/keyspec_left_single_angle_quote,!text/keyspec_less_than_equal,!text/keyspec_left_double_angle_quote", + /* morekeys_greater_than */ "!fixedColumnOrder!3,!text/keyspec_right_single_angle_quote,!text/keyspec_greater_than_equal,!text/keyspec_right_double_angle_quote", + // U+00A1: "¡" INVERTED EXCLAMATION MARK + /* morekeys_exclamation */ "\u00A1", + /* morekeys_currency */ "$,\u00A2,\u20AC,\u00A3,\u00A5,\u20B1", + // U+00B9: "¹" SUPERSCRIPT ONE + // U+00BD: "½" VULGAR FRACTION ONE HALF + // U+2153: "⅓" VULGAR FRACTION ONE THIRD + // U+00BC: "¼" VULGAR FRACTION ONE QUARTER + // U+215B: "⅛" VULGAR FRACTION ONE EIGHTH + /* morekeys_symbols_1 */ "\u00B9,\u00BD,\u2153,\u00BC,\u215B", + // U+00B2: "²" SUPERSCRIPT TWO + // U+2154: "⅔" VULGAR FRACTION TWO THIRDS + /* morekeys_symbols_2 */ "\u00B2,\u2154", + // U+00B3: "³" SUPERSCRIPT THREE + // U+00BE: "¾" VULGAR FRACTION THREE QUARTERS + // U+215C: "⅜" VULGAR FRACTION THREE EIGHTHS + /* morekeys_symbols_3 */ "\u00B3,\u00BE,\u215C", + // U+2074: "⁴" SUPERSCRIPT FOUR + /* morekeys_symbols_4 */ "\u2074", + // U+215D: "⅝" VULGAR FRACTION FIVE EIGHTHS + /* morekeys_symbols_5 */ "\u215D", + /* morekeys_symbols_6 */ EMPTY, + // U+215E: "⅞" VULGAR FRACTION SEVEN EIGHTHS + /* morekeys_symbols_7 */ "\u215E", + /* morekeys_symbols_8 */ EMPTY, + /* morekeys_symbols_9 */ EMPTY, + // U+207F: "ⁿ" SUPERSCRIPT LATIN SMALL LETTER N + // U+2205: "∅" EMPTY SET + /* morekeys_symbols_0 */ "\u207F,\u2205", + /* morekeys_am_pm */ "!fixedColumnOrder!2,!hasLabels!,!text/keylabel_time_am,!text/keylabel_time_pm", + /* keyspec_settings */ "!icon/settings_key|!code/key_settings", + /* keyspec_shortcut */ "!icon/shortcut_key|!code/key_shortcut", + /* keyspec_action_next */ "!hasLabels!,!text/label_next_key|!code/key_action_next", + /* keyspec_action_previous */ "!hasLabels!,!text/label_previous_key|!code/key_action_previous", + // Label for "switch to more symbol" modifier key ("= \ <"). Must be short to fit on key! + /* keylabel_to_more_symbol */ "= \\\\ <", + // Label for "switch to more symbol" modifier key on tablets. Must be short to fit on key! + /* keylabel_tablet_to_more_symbol */ "~ [ <", + // Label for "switch to phone numeric" key. Must be short to fit on key! + /* keylabel_to_phone_numeric */ "123", + // Label for "switch to phone symbols" key. Must be short to fit on key! + // U+FF0A: "*" FULLWIDTH ASTERISK + // U+FF03: "#" FULLWIDTH NUMBER SIGN + /* keylabel_to_phone_symbols */ "\uFF0A\uFF03", + // Key label for "ante meridiem" + /* keylabel_time_am */ "AM", + // Key label for "post meridiem" + /* keylabel_time_pm */ "PM", + /* keyspec_popular_domain */ ".com", + // popular web domains for the locale - most popular, displayed on the keyboard + /* morekeys_popular_domain */ "!hasLabels!,.net,.org,.gov,.edu", + /* keyspecs_left_parenthesis_more_keys */ "!text/keyspec_less_than,!text/keyspec_left_curly_bracket,!text/keyspec_left_square_bracket", + /* keyspecs_right_parenthesis_more_keys */ "!text/keyspec_greater_than,!text/keyspec_right_curly_bracket,!text/keyspec_right_square_bracket", + // The following characters don't need BIDI mirroring. + // U+2018: "‘" LEFT SINGLE QUOTATION MARK + // U+2019: "’" RIGHT SINGLE QUOTATION MARK + // U+201A: "‚" SINGLE LOW-9 QUOTATION MARK + // U+201C: "“" LEFT DOUBLE QUOTATION MARK + // U+201D: "”" RIGHT DOUBLE QUOTATION MARK + // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK + // Abbreviations are: + // laqm: LEFT-POINTING ANGLE QUOTATION MARK + // raqm: RIGHT-POINTING ANGLE QUOTATION MARK + // lqm: LEFT QUOTATION MARK + // rqm: RIGHT QUOTATION MARK + // 9qm: LOW-9 QUOTATION MARK + // The following each quotation mark pair consist of + // <opening quotation mark>, <closing quotation mark> + // and is named after (single|double)_<opening quotation mark>_<closing quotation mark>. + /* single_laqm_raqm */ "!text/keyspec_left_single_angle_quote,!text/keyspec_right_single_angle_quote", + /* single_raqm_laqm */ "!text/keyspec_right_single_angle_quote,!text/keyspec_left_single_angle_quote", + /* double_laqm_raqm */ "!text/keyspec_left_double_angle_quote,!text/keyspec_right_double_angle_quote", + /* double_raqm_laqm */ "!text/keyspec_right_double_angle_quote,!text/keyspec_left_double_angle_quote", + // The following each quotation mark triplet consists of + // <another quotation mark>, <opening quotation mark>, <closing quotation mark> + // and is named after (single|double)_<opening quotation mark>_<closing quotation mark>. + /* single_lqm_rqm */ "\u201A,\u2018,\u2019", + /* single_9qm_lqm */ "\u2019,\u201A,\u2018", + /* single_9qm_rqm */ "\u2018,\u201A,\u2019", + /* single_rqm_9qm */ "\u2018,\u2019,\u201A", + /* double_lqm_rqm */ "\u201E,\u201C,\u201D", + /* double_9qm_lqm */ "\u201D,\u201E,\u201C", + /* double_9qm_rqm */ "\u201C,\u201E,\u201D", + /* double_rqm_9qm */ "\u201C,\u201D,\u201E", + /* morekeys_single_quote */ "!fixedColumnOrder!5,!text/single_quotes,!text/single_angle_quotes", + /* morekeys_double_quote */ "!fixedColumnOrder!5,!text/double_quotes,!text/double_angle_quotes", + /* morekeys_tablet_double_quote */ "!fixedColumnOrder!6,!text/double_quotes,!text/single_quotes,!text/double_angle_quotes,!text/single_angle_quotes", + /* keyspec_emoji_key */ "!icon/emoji_key|!code/key_emoji", + }; + + /* Locale af: Afrikaans */ + private static final String[] TEXTS_af = { + // This is the same as Dutch except more keys of y and demoting vowels with diaeresis. + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + /* morekeys_a */ "\u00E1,\u00E2,\u00E4,\u00E0,\u00E6,\u00E3,\u00E5,\u0101", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + /* morekeys_o */ "\u00F3,\u00F4,\u00F6,\u00F2,\u00F5,\u0153,\u00F8,\u014D", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FA,\u00FB,\u00FC,\u00F9,\u016B", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* morekeys_e */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + // U+0133: "ij" LATIN SMALL LIGATURE IJ + /* morekeys_i */ "\u00ED,\u00EC,\u00EF,\u00EE,\u012F,\u012B,\u0133", + /* keylabel_to_alpha ~ */ + null, null, null, + /* ~ double_quotes */ + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* morekeys_n */ "\u00F1,\u0144", + /* single_quotes */ null, + /* morekeys_s */ null, + // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE + // U+0133: "ij" LATIN SMALL LIGATURE IJ + /* morekeys_y */ "\u00FD,\u0133", + }; + + /* Locale ar: Arabic */ + private static final String[] TEXTS_ar = { + /* morekeys_a ~ */ + null, null, null, null, null, + /* ~ morekeys_i */ + // Label for "switch to alphabetic" key. + // U+0623: "أ" ARABIC LETTER ALEF WITH HAMZA ABOVE + // U+200C: ZERO WIDTH NON-JOINER + // U+0628: "ب" ARABIC LETTER BEH + // U+062C: "ج" ARABIC LETTER JEEM + /* keylabel_to_alpha */ "\u0623\u200C\u0628\u200C\u062C", + /* morekeys_c ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_cyrillic_soft_sign */ + // U+0661: "١" ARABIC-INDIC DIGIT ONE + /* keyspec_symbols_1 */ "\u0661", + // U+0662: "٢" ARABIC-INDIC DIGIT TWO + /* keyspec_symbols_2 */ "\u0662", + // U+0663: "٣" ARABIC-INDIC DIGIT THREE + /* keyspec_symbols_3 */ "\u0663", + // U+0664: "٤" ARABIC-INDIC DIGIT FOUR + /* keyspec_symbols_4 */ "\u0664", + // U+0665: "٥" ARABIC-INDIC DIGIT FIVE + /* keyspec_symbols_5 */ "\u0665", + // U+0666: "٦" ARABIC-INDIC DIGIT SIX + /* keyspec_symbols_6 */ "\u0666", + // U+0667: "٧" ARABIC-INDIC DIGIT SEVEN + /* keyspec_symbols_7 */ "\u0667", + // U+0668: "٨" ARABIC-INDIC DIGIT EIGHT + /* keyspec_symbols_8 */ "\u0668", + // U+0669: "٩" ARABIC-INDIC DIGIT NINE + /* keyspec_symbols_9 */ "\u0669", + // U+0660: "٠" ARABIC-INDIC DIGIT ZERO + /* keyspec_symbols_0 */ "\u0660", + // Label for "switch to symbols" key. + // U+061F: "؟" ARABIC QUESTION MARK + /* keylabel_to_symbol */ "\u0663\u0662\u0661\u061F", + /* additional_morekeys_symbols_1 */ "1", + /* additional_morekeys_symbols_2 */ "2", + /* additional_morekeys_symbols_3 */ "3", + /* additional_morekeys_symbols_4 */ "4", + /* additional_morekeys_symbols_5 */ "5", + /* additional_morekeys_symbols_6 */ "6", + /* additional_morekeys_symbols_7 */ "7", + /* additional_morekeys_symbols_8 */ "8", + /* additional_morekeys_symbols_9 */ "9", + // U+066B: "٫" ARABIC DECIMAL SEPARATOR + // U+066C: "٬" ARABIC THOUSANDS SEPARATOR + /* additional_morekeys_symbols_0 */ "0,\u066B,\u066C", + /* morekeys_nordic_row2_11 */ null, + /* morekeys_punctuation */ null, + // U+061F: "؟" ARABIC QUESTION MARK + // U+060C: "،" ARABIC COMMA + // U+061B: "؛" ARABIC SEMICOLON + /* keyspec_tablet_comma */ "\u060C", + /* keyspec_swiss_row1_11 ~ */ + null, null, null, null, null, null, + /* ~ morekeys_swiss_row2_11 */ + // U+2605: "★" BLACK STAR + // U+066D: "٭" ARABIC FIVE POINTED STAR + /* morekeys_star */ "\u2605,\u066D", + // U+2264: "≤" LESS-THAN OR EQUAL TO + // U+2265: "≥" GREATER-THAN EQUAL TO + // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK + // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + /* keyspec_left_parenthesis */ "(|)", + /* keyspec_right_parenthesis */ ")|(", + /* keyspec_left_square_bracket */ "[|]", + /* keyspec_right_square_bracket */ "]|[", + /* keyspec_left_curly_bracket */ "{|}", + /* keyspec_right_curly_bracket */ "}|{", + /* keyspec_less_than */ "<|>", + /* keyspec_greater_than */ ">|<", + /* keyspec_less_than_equal */ "\u2264|\u2265", + /* keyspec_greater_than_equal */ "\u2265|\u2264", + /* keyspec_left_double_angle_quote */ "\u00AB|\u00BB", + /* keyspec_right_double_angle_quote */ "\u00BB|\u00AB", + /* keyspec_left_single_angle_quote */ "\u2039|\u203A", + /* keyspec_right_single_angle_quote */ "\u203A|\u2039", + /* morekeys_tablet_comma */ "!fixedColumnOrder!4,:,!,\u061F,\u061B,-,\",\'", + // U+0651: "ّ" ARABIC SHADDA + /* keyhintlabel_period */ "\u0651", + /* morekeys_tablet_period */ "!text/morekeys_arabic_diacritics", + // U+00BF: "¿" INVERTED QUESTION MARK + /* morekeys_question */ "?,\u00BF", + /* morekeys_h ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ keyspec_spanish_row2_10 */ + // U+266A: "♪" EIGHTH NOTE + /* morekeys_bullet */ "\u266A", + // The all letters need to be mirrored are found at + // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt + // U+FD3E: "﴾" ORNATE LEFT PARENTHESIS + // U+FD3F: "﴿" ORNATE RIGHT PARENTHESIS + /* morekeys_left_parenthesis */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,!text/keyspecs_left_parenthesis_more_keys", + /* morekeys_right_parenthesis */ "!fixedColumnOrder!4,\uFD3F|\uFD3E,!text/keyspecs_right_parenthesis_more_keys", + // U+0655: "ٕ" ARABIC HAMZA BELOW + // U+0654: "ٔ" ARABIC HAMZA ABOVE + // U+0652: "ْ" ARABIC SUKUN + // U+064D: "ٍ" ARABIC KASRATAN + // U+064C: "ٌ" ARABIC DAMMATAN + // U+064B: "ً" ARABIC FATHATAN + // U+0651: "ّ" ARABIC SHADDA + // U+0656: "ٖ" ARABIC SUBSCRIPT ALEF + // U+0670: "ٰ" ARABIC LETTER SUPERSCRIPT ALEF + // U+0653: "ٓ" ARABIC MADDAH ABOVE + // U+0650: "ِ" ARABIC KASRA + // U+064F: "ُ" ARABIC DAMMA + // U+064E: "َ" ARABIC FATHA + // U+0640: "ـ" ARABIC TATWEEL + // In order to make Tatweel easily distinguishable from other punctuations, we use consecutive Tatweels only for its displayed label. + // Note: The space character is needed as a preceding letter to draw Arabic diacritics characters correctly. + /* morekeys_arabic_diacritics */ "!fixedColumnOrder!7, \u0655|\u0655, \u0654|\u0654, \u0652|\u0652, \u064D|\u064D, \u064C|\u064C, \u064B|\u064B, \u0651|\u0651, \u0656|\u0656, \u0670|\u0670, \u0653|\u0653, \u0650|\u0650, \u064F|\u064F, \u064E|\u064E,\u0640\u0640\u0640|\u0640", + // U+060C: "،" ARABIC COMMA + /* keyspec_comma */ "\u060C", + /* keyhintlabel_tablet_comma */ "\u061F", + /* keyspec_period */ null, + /* morekeys_period */ "!text/morekeys_arabic_diacritics", + /* keyspec_tablet_period */ null, + /* keyhintlabel_tablet_period */ "\u0651", + /* keyspec_symbols_question */ "\u061F", + /* keyspec_symbols_semicolon */ "\u061B", + // U+066A: "٪" ARABIC PERCENT SIGN + /* keyspec_symbols_percent */ "\u066A", + /* morekeys_symbols_semicolon */ ";", + // U+2030: "‰" PER MILLE SIGN + /* morekeys_symbols_percent */ "\\%,\u2030", + }; + + /* Locale az_AZ: Azerbaijani (Azerbaijan) */ + private static final String[] TEXTS_az_AZ = { + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + /* morekeys_a */ "\u00E2", + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + /* morekeys_o */ "\u00F6,\u00F4,\u0153,\u00F2,\u00F3,\u00F5,\u00F8,\u014D", + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FC,\u00FB,\u00F9,\u00FA,\u016B", + // U+0259: "ə" LATIN SMALL LETTER SCHWA + /* morekeys_e */ "\u0259", + // U+0131: "ı" LATIN SMALL LETTER DOTLESS I + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + /* morekeys_i */ "\u0131,\u00EE,\u00EF,\u00EC,\u00ED,\u012F,\u012B", + /* keylabel_to_alpha */ null, + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + /* morekeys_c */ "\u00E7,\u0107,\u010D", + /* double_quotes ~ */ + null, null, null, + /* ~ single_quotes */ + // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE + // U+0161: "š" LATIN SMALL LETTER S WITH CARON + /* morekeys_s */ "\u015F,\u00DF,\u015B,\u0161", + /* morekeys_y ~ */ + null, null, null, null, null, null, + /* ~ morekeys_l */ + // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE + /* morekeys_g */ "\u011F", + }; + + /* Locale be_BY: Belarusian (Belarus) */ + private static final String[] TEXTS_be_BY = { + /* morekeys_a ~ */ + null, null, null, null, null, + /* ~ morekeys_i */ + // Label for "switch to alphabetic" key. + // U+0410: "А" CYRILLIC CAPITAL LETTER A + // U+0411: "Б" CYRILLIC CAPITAL LETTER BE + // U+0412: "В" CYRILLIC CAPITAL LETTER VE + /* keylabel_to_alpha */ "\u0410\u0411\u0412", + /* morekeys_c */ null, + /* double_quotes */ "!text/double_9qm_lqm", + /* morekeys_n */ null, + /* single_quotes */ "!text/single_9qm_lqm", + /* morekeys_s ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_k */ + // U+0451: "ё" CYRILLIC SMALL LETTER IO + /* morekeys_cyrillic_ie */ "\u0451", + /* keyspec_nordic_row1_11 ~ */ + null, null, null, null, + /* ~ morekeys_nordic_row2_10 */ + // U+045E: "ў" CYRILLIC SMALL LETTER SHORT U + /* keyspec_east_slavic_row1_9 */ "\u045E", + // U+044B: "ы" CYRILLIC SMALL LETTER YERU + /* keyspec_east_slavic_row2_2 */ "\u044B", + // U+044D: "э" CYRILLIC SMALL LETTER E + /* keyspec_east_slavic_row2_11 */ "\u044D", + // U+0456: "і" CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I + /* keyspec_east_slavic_row3_5 */ "\u0456", + // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN + /* morekeys_cyrillic_soft_sign */ "\u044A", + }; + + /* Locale bg: Bulgarian */ + private static final String[] TEXTS_bg = { + /* morekeys_a ~ */ + null, null, null, null, null, + /* ~ morekeys_i */ + // Label for "switch to alphabetic" key. + // U+0410: "А" CYRILLIC CAPITAL LETTER A + // U+0411: "Б" CYRILLIC CAPITAL LETTER BE + // U+0412: "В" CYRILLIC CAPITAL LETTER VE + /* keylabel_to_alpha */ "\u0410\u0411\u0412", + /* morekeys_c */ null, + // single_quotes of Bulgarian is default single_quotes_right_left. + /* double_quotes */ "!text/double_9qm_lqm", + }; + + /* Locale bn: Bengali */ + private static final String[] TEXTS_bn = { + /* morekeys_a ~ */ + null, null, null, null, null, + /* ~ morekeys_i */ + // Label for "switch to alphabetic" key. + // U+0995: "क" BENGALI LETTER KA + // U+0996: "ख" BENGALI LETTER KHA + // U+0997: "ग" BENGALI LETTER GA + /* keylabel_to_alpha */ "\u0995\u0996\u0997", + /* morekeys_c ~ */ + null, null, null, null, null, null, null, null, + /* ~ morekeys_z */ + // U+09F3: "৳" BENGALI RUPEE SIGN + /* keyspec_currency */ "\u09F3", + }; + + /* Locale ca: Catalan */ + private static final String[] TEXTS_ca = { + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // U+00AA: "ª" FEMININE ORDINAL INDICATOR + /* morekeys_a */ "\u00E0,\u00E1,\u00E4,\u00E2,\u00E3,\u00E5,\u0105,\u00E6,\u0101,\u00AA", + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + // U+00BA: "º" MASCULINE ORDINAL INDICATOR + /* morekeys_o */ "\u00F2,\u00F3,\u00F6,\u00F4,\u00F5,\u00F8,\u0153,\u014D,\u00BA", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* morekeys_e */ "\u00E8,\u00E9,\u00EB,\u00EA,\u0119,\u0117,\u0113", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + /* morekeys_i */ "\u00ED,\u00EF,\u00EC,\u00EE,\u012F,\u012B", + /* keylabel_to_alpha */ null, + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + /* morekeys_c */ "\u00E7,\u0107,\u010D", + /* double_quotes */ null, + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* morekeys_n */ "\u00F1,\u0144", + /* single_quotes ~ */ + null, null, null, null, null, null, null, + /* ~ morekeys_t */ + // U+00B7: "·" MIDDLE DOT + // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE + /* morekeys_l */ "l\u00B7l,\u0142", + /* morekeys_g ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, + /* ~ morekeys_nordic_row2_11 */ + // U+00B7: "·" MIDDLE DOT + /* morekeys_punctuation */ "!autoColumnOrder!9,\\,,?,!,\u00B7,#,),(,/,;,',@,:,-,\",+,\\%,&", + /* keyspec_tablet_comma ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, + /* ~ keyspec_south_slavic_row3_8 */ + /* morekeys_tablet_punctuation */ "!autoColumnOrder!8,\\,,',\u00B7,#,),(,/,;,@,:,-,\",+,\\%,&", + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + /* keyspec_spanish_row2_10 */ "\u00E7", + }; + + /* Locale cs: Czech */ + private static final String[] TEXTS_cs = { + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + /* morekeys_a */ "\u00E1,\u00E0,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + /* morekeys_o */ "\u00F3,\u00F6,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FA,\u016F,\u00FB,\u00FC,\u00F9,\u016B", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+011B: "ě" LATIN SMALL LETTER E WITH CARON + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* morekeys_e */ "\u00E9,\u011B,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + /* morekeys_i */ "\u00ED,\u00EE,\u00EF,\u00EC,\u012F,\u012B", + /* keylabel_to_alpha */ null, + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + /* morekeys_c */ "\u010D,\u00E7,\u0107", + /* double_quotes */ "!text/double_9qm_lqm", + // U+0148: "ň" LATIN SMALL LETTER N WITH CARON + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* morekeys_n */ "\u0148,\u00F1,\u0144", + /* single_quotes */ "!text/single_9qm_lqm", + // U+0161: "š" LATIN SMALL LETTER S WITH CARON + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE + /* morekeys_s */ "\u0161,\u00DF,\u015B", + // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE + // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS + /* morekeys_y */ "\u00FD,\u00FF", + // U+010F: "ď" LATIN SMALL LETTER D WITH CARON + /* morekeys_d */ "\u010F", + // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON + // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE + // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE + /* morekeys_z */ "\u017E,\u017A,\u017C", + /* keyspec_currency */ null, + // U+0165: "ť" LATIN SMALL LETTER T WITH CARON + /* morekeys_t */ "\u0165", + /* morekeys_l */ null, + /* morekeys_g */ null, + /* single_angle_quotes */ "!text/single_raqm_laqm", + /* double_angle_quotes */ "!text/double_raqm_laqm", + // U+0159: "ř" LATIN SMALL LETTER R WITH CARON + /* morekeys_r */ "\u0159", + }; + + /* Locale da: Danish */ + private static final String[] TEXTS_da = { + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + /* morekeys_a */ "\u00E1,\u00E4,\u00E0,\u00E2,\u00E3,\u0101", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + /* morekeys_o */ "\u00F3,\u00F4,\u00F2,\u00F5,\u0153,\u014D", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FA,\u00FC,\u00FB,\u00F9,\u016B", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + /* morekeys_e */ "\u00E9,\u00EB", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + /* morekeys_i */ "\u00ED,\u00EF", + /* keylabel_to_alpha */ null, + /* morekeys_c */ null, + /* double_quotes */ "!text/double_9qm_lqm", + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* morekeys_n */ "\u00F1,\u0144", + /* single_quotes */ "!text/single_9qm_lqm", + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE + // U+0161: "š" LATIN SMALL LETTER S WITH CARON + /* morekeys_s */ "\u00DF,\u015B,\u0161", + // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE + // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS + /* morekeys_y */ "\u00FD,\u00FF", + // U+00F0: "ð" LATIN SMALL LETTER ETH + /* morekeys_d */ "\u00F0", + /* morekeys_z ~ */ + null, null, null, + /* ~ morekeys_t */ + // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE + /* morekeys_l */ "\u0142", + /* morekeys_g */ null, + /* single_angle_quotes */ "!text/single_raqm_laqm", + /* double_angle_quotes */ "!text/double_raqm_laqm", + /* morekeys_r ~ */ + null, null, null, + /* ~ morekeys_cyrillic_ie */ + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + /* keyspec_nordic_row1_11 */ "\u00E5", + // U+00E6: "æ" LATIN SMALL LETTER AE + /* keyspec_nordic_row2_10 */ "\u00E6", + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + /* keyspec_nordic_row2_11 */ "\u00F8", + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + /* morekeys_nordic_row2_10 */ "\u00E4", + /* keyspec_east_slavic_row1_9 ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, + /* ~ additional_morekeys_symbols_0 */ + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + /* morekeys_nordic_row2_11 */ "\u00F6", + }; + + /* Locale de: German */ + private static final String[] TEXTS_de = { + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + /* morekeys_a */ "\u00E4,%,\u00E2,\u00E0,\u00E1,\u00E6,\u00E3,\u00E5,\u0101", + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + /* morekeys_o */ "\u00F6,%,\u00F4,\u00F2,\u00F3,\u00F5,\u0153,\u00F8,\u014D", + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FC,%,\u00FB,\u00F9,\u00FA,\u016B", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + /* morekeys_e */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0117", + /* morekeys_i ~ */ + null, null, null, + /* ~ morekeys_c */ + /* double_quotes */ "!text/double_9qm_lqm", + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* morekeys_n */ "\u00F1,\u0144", + /* single_quotes */ "!text/single_9qm_lqm", + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE + // U+0161: "š" LATIN SMALL LETTER S WITH CARON + /* morekeys_s */ "\u00DF,\u015B,\u0161", + /* morekeys_y ~ */ + null, null, null, null, null, null, null, + /* ~ morekeys_g */ + /* single_angle_quotes */ "!text/single_raqm_laqm", + /* double_angle_quotes */ "!text/double_raqm_laqm", + /* morekeys_r ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, + /* ~ keyspec_tablet_comma */ + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + /* keyspec_swiss_row1_11 */ "\u00FC", + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + /* keyspec_swiss_row2_10 */ "\u00F6", + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + /* keyspec_swiss_row2_11 */ "\u00E4", + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + /* morekeys_swiss_row1_11 */ "\u00E8", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + /* morekeys_swiss_row2_10 */ "\u00E9", + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + /* morekeys_swiss_row2_11 */ "\u00E0", + }; + + /* Locale el: Greek */ + private static final String[] TEXTS_el = { + /* morekeys_a ~ */ + null, null, null, null, null, + /* ~ morekeys_i */ + // Label for "switch to alphabetic" key. + // U+0391: "Α" GREEK CAPITAL LETTER ALPHA + // U+0392: "Β" GREEK CAPITAL LETTER BETA + // U+0393: "Γ" GREEK CAPITAL LETTER GAMMA + /* keylabel_to_alpha */ "\u0391\u0392\u0393", + }; + + /* Locale en: English */ + private static final String[] TEXTS_en = { + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + /* morekeys_a */ "\u00E0,\u00E1,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101", + // U+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+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + /* morekeys_o */ "\u00F3,\u00F4,\u00F6,\u00F2,\u0153,\u00F8,\u014D,\u00F5", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FA,\u00FB,\u00FC,\u00F9,\u016B", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* morekeys_e */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0113", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + /* morekeys_i */ "\u00ED,\u00EE,\u00EF,\u012B,\u00EC", + /* keylabel_to_alpha */ null, + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + /* morekeys_c */ "\u00E7", + /* double_quotes */ null, + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + /* morekeys_n */ "\u00F1", + /* single_quotes */ null, + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + /* morekeys_s */ "\u00DF", + }; + + /* Locale eo: Esperanto */ + private static final String[] TEXTS_eo = { + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // U+0103: "ă" LATIN SMALL LETTER A WITH BREVE + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + // U+00AA: "ª" FEMININE ORDINAL INDICATOR + /* morekeys_a */ "\u00E1,\u00E0,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101,\u0103,\u0105,\u00AA", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE + // U+00BA: "º" MASCULINE ORDINAL INDICATOR + /* morekeys_o */ "\u00F3,\u00F6,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D,\u0151,\u00BA", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + // U+0169: "ũ" LATIN SMALL LETTER U WITH TILDE + // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE + // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK + // U+00B5: "µ" MICRO SIGN + /* morekeys_u */ "\u00FA,\u016F,\u00FB,\u00FC,\u00F9,\u016B,\u0169,\u0171,\u0173,\u00B5", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+011B: "ě" LATIN SMALL LETTER E WITH CARON + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* morekeys_e */ "\u00E9,\u011B,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+0129: "ĩ" LATIN SMALL LETTER I WITH TILDE + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + // U+0131: "ı" LATIN SMALL LETTER DOTLESS I + // U+0133: "ij" LATIN SMALL LIGATURE IJ + /* morekeys_i */ "\u00ED,\u00EE,\u00EF,\u0129,\u00EC,\u012F,\u012B,\u0131,\u0133", + /* keylabel_to_alpha */ null, + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+010B: "ċ" LATIN SMALL LETTER C WITH DOT ABOVE + /* morekeys_c */ "\u0107,\u010D,\u00E7,\u010B", + /* double_quotes */ null, + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA + // U+0148: "ň" LATIN SMALL LETTER N WITH CARON + // U+0149: "ʼn" LATIN SMALL LETTER N PRECEDED BY APOSTROPHE + // U+014B: "ŋ" LATIN SMALL LETTER ENG + /* morekeys_n */ "\u00F1,\u0144,\u0146,\u0148,\u0149,\u014B", + /* single_quotes */ null, + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + // U+0161: "š" LATIN SMALL LETTER S WITH CARON + // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE + // U+0219: "ș" LATIN SMALL LETTER S WITH COMMA BELOW + // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA + /* morekeys_s */ "\u00DF,\u0161,\u015B,\u0219,\u015F", + // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE + // U+0177: "ŷ" LATIN SMALL LETTER Y WITH CIRCUMFLEX + // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS + // U+00FE: "þ" LATIN SMALL LETTER THORN + /* morekeys_y */ "y,\u00FD,\u0177,\u00FF,\u00FE", + // U+00F0: "ð" LATIN SMALL LETTER ETH + // U+010F: "ď" LATIN SMALL LETTER D WITH CARON + // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE + /* morekeys_d */ "\u00F0,\u010F,\u0111", + // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE + // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE + // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON + /* morekeys_z */ "\u017A,\u017C,\u017E", + /* keyspec_currency */ null, + // U+0165: "ť" LATIN SMALL LETTER T WITH CARON + // U+021B: "ț" LATIN SMALL LETTER T WITH COMMA BELOW + // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA + // U+0167: "ŧ" LATIN SMALL LETTER T WITH STROKE + /* morekeys_t */ "\u0165,\u021B,\u0163,\u0167", + // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE + // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA + // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON + // U+0140: "ŀ" LATIN SMALL LETTER L WITH MIDDLE DOT + // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE + /* morekeys_l */ "\u013A,\u013C,\u013E,\u0140,\u0142", + // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE + // U+0121: "ġ" LATIN SMALL LETTER G WITH DOT ABOVE + // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA + /* morekeys_g */ "\u011F,\u0121,\u0123", + /* single_angle_quotes */ null, + /* double_angle_quotes */ null, + // U+0159: "ř" LATIN SMALL LETTER R WITH CARON + // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE + // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA + /* morekeys_r */ "\u0159,\u0155,\u0157", + // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA + // U+0138: "ĸ" LATIN SMALL LETTER KRA + /* morekeys_k */ "\u0137,\u0138", + /* morekeys_cyrillic_ie ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_question */ + // U+0125: "ĥ" LATIN SMALL LETTER H WITH CIRCUMFLEX + // U+0127: "ħ" LATIN SMALL LETTER H WITH STROKE + /* morekeys_h */ "\u0125,\u0127", + // U+0175: "ŵ" LATIN SMALL LETTER W WITH CIRCUMFLEX + /* morekeys_w */ "w,\u0175", + /* morekeys_east_slavic_row2_2 ~ */ + null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_tablet_punctuation */ + // U+0135: "ĵ" LATIN SMALL LETTER J WITH CIRCUMFLEX + /* keyspec_spanish_row2_10 */ "\u0135", + /* morekeys_bullet ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_symbols_percent */ + // U+0175: "ŵ" LATIN SMALL LETTER W WITH CIRCUMFLEX + /* morekeys_v */ "w,\u0175", + /* morekeys_j */ null, + /* morekeys_q */ "q", + /* morekeys_x */ "x", + // U+015D: "ŝ" LATIN SMALL LETTER S WITH CIRCUMFLEX + /* keyspec_q */ "\u015D", + // U+011D: "ĝ" LATIN SMALL LETTER G WITH CIRCUMFLEX + /* keyspec_w */ "\u011D", + // U+016D: "ŭ" LATIN SMALL LETTER U WITH BREVE + /* keyspec_y */ "\u016D", + // U+0109: "ĉ" LATIN SMALL LETTER C WITH CIRCUMFLEX + /* keyspec_x */ "\u0109", + }; + + /* Locale es: Spanish */ + private static final String[] TEXTS_es = { + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // U+00AA: "ª" FEMININE ORDINAL INDICATOR + /* morekeys_a */ "\u00E1,\u00E0,\u00E4,\u00E2,\u00E3,\u00E5,\u0105,\u00E6,\u0101,\u00AA", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + // U+00BA: "º" MASCULINE ORDINAL INDICATOR + /* morekeys_o */ "\u00F3,\u00F2,\u00F6,\u00F4,\u00F5,\u00F8,\u0153,\u014D,\u00BA", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* morekeys_e */ "\u00E9,\u00E8,\u00EB,\u00EA,\u0119,\u0117,\u0113", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + /* morekeys_i */ "\u00ED,\u00EF,\u00EC,\u00EE,\u012F,\u012B", + /* keylabel_to_alpha */ null, + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + /* morekeys_c */ "\u00E7,\u0107,\u010D", + /* double_quotes */ null, + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* morekeys_n */ "\u00F1,\u0144", + /* single_quotes ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_nordic_row2_11 */ + // U+00A1: "¡" INVERTED EXCLAMATION MARK + // U+00BF: "¿" INVERTED QUESTION MARK + /* morekeys_punctuation */ "!autoColumnOrder!9,\\,,?,!,#,),(,/,;,\u00A1,',@,:,-,\",+,\\%,&,\u00BF", + }; + + /* Locale et_EE: Estonian (Estonia) */ + private static final String[] TEXTS_et_EE = { + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + /* morekeys_a */ "\u00E4,\u0101,\u00E0,\u00E1,\u00E2,\u00E3,\u00E5,\u00E6,\u0105", + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + /* morekeys_o */ "\u00F6,\u00F5,\u00F2,\u00F3,\u00F4,\u0153,\u0151,\u00F8", + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE + // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE + /* morekeys_u */ "\u00FC,\u016B,\u0173,\u00F9,\u00FA,\u00FB,\u016F,\u0171", + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+011B: "ě" LATIN SMALL LETTER E WITH CARON + /* morekeys_e */ "\u0113,\u00E8,\u0117,\u00E9,\u00EA,\u00EB,\u0119,\u011B", + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+0131: "ı" LATIN SMALL LETTER DOTLESS I + /* morekeys_i */ "\u012B,\u00EC,\u012F,\u00ED,\u00EE,\u00EF,\u0131", + /* keylabel_to_alpha */ null, + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + /* morekeys_c */ "\u010D,\u00E7,\u0107", + /* double_quotes */ "!text/double_9qm_lqm", + // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* morekeys_n */ "\u0146,\u00F1,\u0144", + /* single_quotes */ "!text/single_9qm_lqm", + // U+0161: "š" LATIN SMALL LETTER S WITH CARON + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE + // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA + /* morekeys_s */ "\u0161,\u00DF,\u015B,\u015F", + // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE + // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS + /* morekeys_y */ "\u00FD,\u00FF", + // U+010F: "ď" LATIN SMALL LETTER D WITH CARON + /* morekeys_d */ "\u010F", + // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON + // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE + // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE + /* morekeys_z */ "\u017E,\u017C,\u017A", + /* keyspec_currency */ null, + // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA + // U+0165: "ť" LATIN SMALL LETTER T WITH CARON + /* morekeys_t */ "\u0163,\u0165", + // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA + // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE + // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE + // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON + /* morekeys_l */ "\u013C,\u0142,\u013A,\u013E", + // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA + // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE + /* morekeys_g */ "\u0123,\u011F", + /* single_angle_quotes */ null, + /* double_angle_quotes */ null, + // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA + // U+0159: "ř" LATIN SMALL LETTER R WITH CARON + // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE + /* morekeys_r */ "\u0157,\u0159,\u0155", + // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA + /* morekeys_k */ "\u0137", + /* morekeys_cyrillic_ie */ null, + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + /* keyspec_nordic_row1_11 */ "\u00FC", + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + /* keyspec_nordic_row2_10 */ "\u00F6", + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + /* keyspec_nordic_row2_11 */ "\u00E4", + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + /* morekeys_nordic_row2_10 */ "\u00F5", + }; + + /* Locale eu_ES: Basque (Spain) */ + private static final String[] TEXTS_eu_ES = { + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // U+00AA: "ª" FEMININE ORDINAL INDICATOR + /* morekeys_a */ "\u00E1,\u00E0,\u00E4,\u00E2,\u00E3,\u00E5,\u0105,\u00E6,\u0101,\u00AA", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + // U+00BA: "º" MASCULINE ORDINAL INDICATOR + /* morekeys_o */ "\u00F3,\u00F2,\u00F6,\u00F4,\u00F5,\u00F8,\u0153,\u014D,\u00BA", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* morekeys_e */ "\u00E9,\u00E8,\u00EB,\u00EA,\u0119,\u0117,\u0113", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + /* morekeys_i */ "\u00ED,\u00EF,\u00EC,\u00EE,\u012F,\u012B", + /* keylabel_to_alpha */ null, + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + /* morekeys_c */ "\u00E7,\u0107,\u010D", + /* double_quotes */ null, + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* morekeys_n */ "\u00F1,\u0144", + }; + + /* Locale fa: Persian */ + private static final String[] TEXTS_fa = { + /* morekeys_a ~ */ + null, null, null, null, null, + /* ~ morekeys_i */ + // Label for "switch to alphabetic" key. + // U+0627: "ا" ARABIC LETTER ALEF + // U+200C: ZERO WIDTH NON-JOINER + // U+0628: "ب" ARABIC LETTER BEH + // U+067E: "پ" ARABIC LETTER PEH + /* keylabel_to_alpha */ "\u0627\u200C\u0628\u200C\u067E", + /* morekeys_c ~ */ + null, null, null, null, null, null, null, null, + /* ~ morekeys_z */ + // U+FDFC: "﷼" RIAL SIGN + /* keyspec_currency */ "\uFDFC", + /* morekeys_t ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, + /* ~ morekeys_cyrillic_soft_sign */ + // U+06F1: "۱" EXTENDED ARABIC-INDIC DIGIT ONE + /* keyspec_symbols_1 */ "\u06F1", + // U+06F2: "۲" EXTENDED ARABIC-INDIC DIGIT TWO + /* keyspec_symbols_2 */ "\u06F2", + // U+06F3: "۳" EXTENDED ARABIC-INDIC DIGIT THREE + /* keyspec_symbols_3 */ "\u06F3", + // U+06F4: "۴" EXTENDED ARABIC-INDIC DIGIT FOUR + /* keyspec_symbols_4 */ "\u06F4", + // U+06F5: "۵" EXTENDED ARABIC-INDIC DIGIT FIVE + /* keyspec_symbols_5 */ "\u06F5", + // U+06F6: "۶" EXTENDED ARABIC-INDIC DIGIT SIX + /* keyspec_symbols_6 */ "\u06F6", + // U+06F7: "۷" EXTENDED ARABIC-INDIC DIGIT SEVEN + /* keyspec_symbols_7 */ "\u06F7", + // U+06F8: "۸" EXTENDED ARABIC-INDIC DIGIT EIGHT + /* keyspec_symbols_8 */ "\u06F8", + // U+06F9: "۹" EXTENDED ARABIC-INDIC DIGIT NINE + /* keyspec_symbols_9 */ "\u06F9", + // U+06F0: "۰" EXTENDED ARABIC-INDIC DIGIT ZERO + /* keyspec_symbols_0 */ "\u06F0", + // Label for "switch to symbols" key. + // U+061F: "؟" ARABIC QUESTION MARK + /* keylabel_to_symbol */ "\u06F3\u06F2\u06F1\u061F", + /* additional_morekeys_symbols_1 */ "1", + /* additional_morekeys_symbols_2 */ "2", + /* additional_morekeys_symbols_3 */ "3", + /* additional_morekeys_symbols_4 */ "4", + /* additional_morekeys_symbols_5 */ "5", + /* additional_morekeys_symbols_6 */ "6", + /* additional_morekeys_symbols_7 */ "7", + /* additional_morekeys_symbols_8 */ "8", + /* additional_morekeys_symbols_9 */ "9", + // U+066B: "٫" ARABIC DECIMAL SEPARATOR + // U+066C: "٬" ARABIC THOUSANDS SEPARATOR + /* additional_morekeys_symbols_0 */ "0,\u066B,\u066C", + /* morekeys_nordic_row2_11 */ null, + /* morekeys_punctuation */ null, + // U+060C: "،" ARABIC COMMA + // U+061B: "؛" ARABIC SEMICOLON + // U+061F: "؟" ARABIC QUESTION MARK + // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + /* keyspec_tablet_comma */ "\u060C", + /* keyspec_swiss_row1_11 ~ */ + null, null, null, null, null, null, + /* ~ morekeys_swiss_row2_11 */ + // U+2605: "★" BLACK STAR + // U+066D: "٭" ARABIC FIVE POINTED STAR + /* morekeys_star */ "\u2605,\u066D", + /* keyspec_left_parenthesis */ "(|)", + /* keyspec_right_parenthesis */ ")|(", + /* keyspec_left_square_bracket */ "[|]", + /* keyspec_right_square_bracket */ "]|[", + /* keyspec_left_curly_bracket */ "{|}", + /* keyspec_right_curly_bracket */ "}|{", + /* keyspec_less_than */ "<|>", + /* keyspec_greater_than */ ">|<", + /* keyspec_less_than_equal */ "\u2264|\u2265", + /* keyspec_greater_than_equal */ "\u2265|\u2264", + /* keyspec_left_double_angle_quote */ "\u00AB|\u00BB", + /* keyspec_right_double_angle_quote */ "\u00BB|\u00AB", + /* keyspec_left_single_angle_quote */ "\u2039|\u203A", + /* keyspec_right_single_angle_quote */ "\u203A|\u2039", + /* morekeys_tablet_comma */ "!fixedColumnOrder!4,:,!,\u061F,\u061B,-,!text/keyspec_left_double_angle_quote,!text/keyspec_right_double_angle_quote", + // U+064B: "ً" ARABIC FATHATAN + /* keyhintlabel_period */ "\u064B", + /* morekeys_tablet_period */ "!text/morekeys_arabic_diacritics", + // U+00BF: "¿" INVERTED QUESTION MARK + /* morekeys_question */ "?,\u00BF", + /* morekeys_h ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ keyspec_spanish_row2_10 */ + // U+266A: "♪" EIGHTH NOTE + /* morekeys_bullet */ "\u266A", + // The all letters need to be mirrored are found at + // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt + // U+FD3E: "﴾" ORNATE LEFT PARENTHESIS + // U+FD3F: "﴿" ORNATE RIGHT PARENTHESIS + /* morekeys_left_parenthesis */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,!text/keyspecs_left_parenthesis_more_keys", + /* morekeys_right_parenthesis */ "!fixedColumnOrder!4,\uFD3F|\uFD3E,!text/keyspecs_right_parenthesis_more_keys", + // U+0655: "ٕ" ARABIC HAMZA BELOW + // U+0652: "ْ" ARABIC SUKUN + // U+0651: "ّ" ARABIC SHADDA + // U+064C: "ٌ" ARABIC DAMMATAN + // U+064D: "ٍ" ARABIC KASRATAN + // U+064B: "ً" ARABIC FATHATAN + // U+0654: "ٔ" ARABIC HAMZA ABOVE + // U+0656: "ٖ" ARABIC SUBSCRIPT ALEF + // U+0670: "ٰ" ARABIC LETTER SUPERSCRIPT ALEF + // U+0653: "ٓ" ARABIC MADDAH ABOVE + // U+064F: "ُ" ARABIC DAMMA + // U+0650: "ِ" ARABIC KASRA + // U+064E: "َ" ARABIC FATHA + // U+0640: "ـ" ARABIC TATWEEL + // In order to make Tatweel easily distinguishable from other punctuations, we use consecutive Tatweels only for its displayed label. + // Note: The space character is needed as a preceding letter to draw Arabic diacritics characters correctly. + /* morekeys_arabic_diacritics */ "!fixedColumnOrder!7, \u0655|\u0655, \u0652|\u0652, \u0651|\u0651, \u064C|\u064C, \u064D|\u064D, \u064B|\u064B, \u0654|\u0654, \u0656|\u0656, \u0670|\u0670, \u0653|\u0653, \u064F|\u064F, \u0650|\u0650, \u064E|\u064E,\u0640\u0640\u0640|\u0640", + // U+060C: "،" ARABIC COMMA + /* keyspec_comma */ "\u060C", + /* keyhintlabel_tablet_comma */ "\u061F", + /* keyspec_period */ null, + /* morekeys_period */ "!text/morekeys_arabic_diacritics", + /* keyspec_tablet_period */ null, + /* keyhintlabel_tablet_period */ "\u064B", + /* keyspec_symbols_question */ "\u061F", + /* keyspec_symbols_semicolon */ "\u061B", + // U+066A: "٪" ARABIC PERCENT SIGN + /* keyspec_symbols_percent */ "\u066A", + /* morekeys_symbols_semicolon */ ";", + // U+2030: "‰" PER MILLE SIGN + /* morekeys_symbols_percent */ "\\%,\u2030", + /* morekeys_v ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_plus */ + // U+2264: "≤" LESS-THAN OR EQUAL TO + // U+2265: "≥" GREATER-THAN EQUAL TO + // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK + // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + /* morekeys_less_than */ "!fixedColumnOrder!3,!text/keyspec_left_single_angle_quote,!text/keyspec_less_than_equal,!text/keyspec_less_than", + /* morekeys_greater_than */ "!fixedColumnOrder!3,!text/keyspec_right_single_angle_quote,!text/keyspec_greater_than_equal,!text/keyspec_greater_than", + }; + + /* Locale fi: Finnish */ + private static final String[] TEXTS_fi = { + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + /* morekeys_a */ "\u00E6,\u00E0,\u00E1,\u00E2,\u00E3,\u0101", + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + /* morekeys_o */ "\u00F8,\u00F4,\u00F2,\u00F3,\u00F5,\u0153,\u014D", + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + /* morekeys_u */ "\u00FC", + /* morekeys_e ~ */ + null, null, null, null, null, null, null, + /* ~ single_quotes */ + // U+0161: "š" LATIN SMALL LETTER S WITH CARON + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE + /* morekeys_s */ "\u0161,\u00DF,\u015B", + /* morekeys_y */ null, + /* morekeys_d */ null, + // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON + // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE + // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE + /* morekeys_z */ "\u017E,\u017A,\u017C", + /* keyspec_currency ~ */ + null, null, null, null, null, null, null, null, null, + /* ~ morekeys_cyrillic_ie */ + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + /* keyspec_nordic_row1_11 */ "\u00E5", + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + /* keyspec_nordic_row2_10 */ "\u00F6", + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + /* keyspec_nordic_row2_11 */ "\u00E4", + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + /* morekeys_nordic_row2_10 */ "\u00F8", + /* keyspec_east_slavic_row1_9 ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, + /* ~ additional_morekeys_symbols_0 */ + // U+00E6: "æ" LATIN SMALL LETTER AE + /* morekeys_nordic_row2_11 */ "\u00E6", + }; + + /* Locale fr: French */ + private static final String[] TEXTS_fr = { + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // U+00AA: "ª" FEMININE ORDINAL INDICATOR + /* morekeys_a */ "\u00E0,\u00E2,%,\u00E6,\u00E1,\u00E4,\u00E3,\u00E5,\u0101,\u00AA", + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + // U+00BA: "º" MASCULINE ORDINAL INDICATOR + /* morekeys_o */ "\u00F4,\u0153,%,\u00F6,\u00F2,\u00F3,\u00F5,\u00F8,\u014D,\u00BA", + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00F9,\u00FB,%,\u00FC,\u00FA,\u016B", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* morekeys_e */ "\u00E9,\u00E8,\u00EA,\u00EB,%,\u0119,\u0117,\u0113", + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + /* morekeys_i */ "\u00EE,%,\u00EF,\u00EC,\u00ED,\u012F,\u012B", + /* keylabel_to_alpha */ null, + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + /* morekeys_c */ "\u00E7,%,\u0107,\u010D", + /* double_quotes ~ */ + null, null, null, null, + /* ~ morekeys_s */ + // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS + /* morekeys_y */ "%,\u00FF", + /* morekeys_d ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ keyspec_tablet_comma */ + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + /* keyspec_swiss_row1_11 */ "\u00E8", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + /* keyspec_swiss_row2_10 */ "\u00E9", + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + /* keyspec_swiss_row2_11 */ "\u00E0", + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + /* morekeys_swiss_row1_11 */ "\u00FC", + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + /* morekeys_swiss_row2_10 */ "\u00F6", + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + /* morekeys_swiss_row2_11 */ "\u00E4", + }; + + /* Locale gl_ES: Gallegan (Spain) */ + private static final String[] TEXTS_gl_ES = { + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // U+00AA: "ª" FEMININE ORDINAL INDICATOR + /* morekeys_a */ "\u00E1,\u00E0,\u00E4,\u00E2,\u00E3,\u00E5,\u0105,\u00E6,\u0101,\u00AA", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + // U+00BA: "º" MASCULINE ORDINAL INDICATOR + /* morekeys_o */ "\u00F3,\u00F2,\u00F6,\u00F4,\u00F5,\u00F8,\u0153,\u014D,\u00BA", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* morekeys_e */ "\u00E9,\u00E8,\u00EB,\u00EA,\u0119,\u0117,\u0113", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + /* morekeys_i */ "\u00ED,\u00EF,\u00EC,\u00EE,\u012F,\u012B", + /* keylabel_to_alpha */ null, + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + /* morekeys_c */ "\u00E7,\u0107,\u010D", + /* double_quotes */ null, + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* morekeys_n */ "\u00F1,\u0144", + }; + + /* Locale hi: Hindi */ + private static final String[] TEXTS_hi = { + /* morekeys_a ~ */ + null, null, null, null, null, + /* ~ morekeys_i */ + // Label for "switch to alphabetic" key. + // U+0915: "क" DEVANAGARI LETTER KA + // U+0916: "ख" DEVANAGARI LETTER KHA + // U+0917: "ग" DEVANAGARI LETTER GA + /* keylabel_to_alpha */ "\u0915\u0916\u0917", + /* morekeys_c ~ */ + null, null, null, null, null, null, null, null, + /* ~ morekeys_z */ + // U+20B9: "₹" INDIAN RUPEE SIGN + /* keyspec_currency */ "\u20B9", + /* morekeys_t ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, + /* ~ morekeys_cyrillic_soft_sign */ + // U+0967: "१" DEVANAGARI DIGIT ONE + /* keyspec_symbols_1 */ "\u0967", + // U+0968: "२" DEVANAGARI DIGIT TWO + /* keyspec_symbols_2 */ "\u0968", + // U+0969: "३" DEVANAGARI DIGIT THREE + /* keyspec_symbols_3 */ "\u0969", + // U+096A: "४" DEVANAGARI DIGIT FOUR + /* keyspec_symbols_4 */ "\u096A", + // U+096B: "५" DEVANAGARI DIGIT FIVE + /* keyspec_symbols_5 */ "\u096B", + // U+096C: "६" DEVANAGARI DIGIT SIX + /* keyspec_symbols_6 */ "\u096C", + // U+096D: "७" DEVANAGARI DIGIT SEVEN + /* keyspec_symbols_7 */ "\u096D", + // U+096E: "८" DEVANAGARI DIGIT EIGHT + /* keyspec_symbols_8 */ "\u096E", + // U+096F: "९" DEVANAGARI DIGIT NINE + /* keyspec_symbols_9 */ "\u096F", + // U+0966: "०" DEVANAGARI DIGIT ZERO + /* keyspec_symbols_0 */ "\u0966", + // Label for "switch to symbols" key. + /* keylabel_to_symbol */ "?\u0967\u0968\u0969", + /* additional_morekeys_symbols_1 */ "1", + /* additional_morekeys_symbols_2 */ "2", + /* additional_morekeys_symbols_3 */ "3", + /* additional_morekeys_symbols_4 */ "4", + /* additional_morekeys_symbols_5 */ "5", + /* additional_morekeys_symbols_6 */ "6", + /* additional_morekeys_symbols_7 */ "7", + /* additional_morekeys_symbols_8 */ "8", + /* additional_morekeys_symbols_9 */ "9", + /* additional_morekeys_symbols_0 */ "0", + }; + + /* Locale hr: Croatian */ + private static final String[] TEXTS_hr = { + /* morekeys_a ~ */ + null, null, null, null, null, null, + /* ~ keylabel_to_alpha */ + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + /* morekeys_c */ "\u010D,\u0107,\u00E7", + /* double_quotes */ "!text/double_9qm_rqm", + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* morekeys_n */ "\u00F1,\u0144", + /* single_quotes */ "!text/single_9qm_rqm", + // U+0161: "š" LATIN SMALL LETTER S WITH CARON + // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + /* morekeys_s */ "\u0161,\u015B,\u00DF", + /* morekeys_y */ null, + // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE + /* morekeys_d */ "\u0111", + // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON + // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE + // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE + /* morekeys_z */ "\u017E,\u017A,\u017C", + /* keyspec_currency ~ */ + null, null, null, null, + /* ~ morekeys_g */ + /* single_angle_quotes */ "!text/single_raqm_laqm", + /* double_angle_quotes */ "!text/double_raqm_laqm", + }; + + /* Locale hu: Hungarian */ + private static final String[] TEXTS_hu = { + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + /* morekeys_a */ "\u00E1,\u00E0,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + /* morekeys_o */ "\u00F3,\u00F6,\u0151,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FA,\u00FC,\u0171,\u00FB,\u00F9,\u016B", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* morekeys_e */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + /* morekeys_i */ "\u00ED,\u00EE,\u00EF,\u00EC,\u012F,\u012B", + /* keylabel_to_alpha */ null, + /* morekeys_c */ null, + /* double_quotes */ "!text/double_9qm_rqm", + /* morekeys_n */ null, + /* single_quotes */ "!text/single_9qm_rqm", + /* morekeys_s ~ */ + null, null, null, null, null, null, null, null, + /* ~ morekeys_g */ + /* single_angle_quotes */ "!text/single_raqm_laqm", + /* double_angle_quotes */ "!text/double_raqm_laqm", + }; + + /* Locale hy_AM: Armenian (Armenia) */ + private static final String[] TEXTS_hy_AM = { + /* morekeys_a ~ */ + null, null, null, null, null, + /* ~ morekeys_i */ + // Label for "switch to alphabetic" key. + // U+0531: "Ա" ARMENIAN CAPITAL LETTER AYB + // U+0532: "Բ" ARMENIAN CAPITAL LETTER BEN + // U+0533: "Գ" ARMENIAN CAPITAL LETTER GIM + /* keylabel_to_alpha */ "\u0531\u0532\u0533", + /* morekeys_c ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, + /* ~ morekeys_nordic_row2_11 */ + // U+055E: "՞" ARMENIAN QUESTION MARK + // U+055C: "՜" ARMENIAN EXCLAMATION MARK + // U+055A: "՚" ARMENIAN APOSTROPHE + // U+0559: "ՙ" ARMENIAN MODIFIER LETTER LEFT HALF RING + // U+055D: "՝" ARMENIAN COMMA + // U+055B: "՛" ARMENIAN EMPHASIS MARK + // U+058A: "֊" ARMENIAN HYPHEN + // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + // U+055F: "՟" ARMENIAN ABBREVIATION MARK + /* morekeys_punctuation */ "!autoColumnOrder!8,\\,,\u055E,\u055C,.,\u055A,\u0559,?,!,\u055D,\u055B,\u058A,\u00BB,\u00AB,\u055F,;,:", + // U+058F: "֏" ARMENIAN DRAM SIGN + // TODO: Enable this when we have glyph for the following letter + // <string name="keyspec_currency">֏</string> + // + // U+055D: "՝" ARMENIAN COMMA + /* keyspec_tablet_comma */ "\u055D", + /* keyspec_swiss_row1_11 ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, + /* ~ keyhintlabel_period */ + /* morekeys_tablet_period */ "!text/morekeys_punctuation", + // U+055E: "՞" ARMENIAN QUESTION MARK + // U+00BF: "¿" INVERTED QUESTION MARK + /* morekeys_question */ "\u055E,\u00BF", + /* morekeys_h ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, + /* ~ keyhintlabel_tablet_comma */ + // U+0589: "։" ARMENIAN FULL STOP + /* keyspec_period */ "\u0589", + /* morekeys_period */ null, + /* keyspec_tablet_period */ "\u0589", + /* keyhintlabel_tablet_period ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, + /* ~ morekeys_greater_than */ + // U+055C: "՜" ARMENIAN EXCLAMATION MARK + // U+00A1: "¡" INVERTED EXCLAMATION MARK + /* morekeys_exclamation */ "\u055C,\u00A1", + }; + + /* Locale is: Icelandic */ + private static final String[] TEXTS_is = { + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + /* morekeys_a */ "\u00E1,\u00E4,\u00E6,\u00E5,\u00E0,\u00E2,\u00E3,\u0101", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + /* morekeys_o */ "\u00F3,\u00F6,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FA,\u00FC,\u00FB,\u00F9,\u016B", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* morekeys_e */ "\u00E9,\u00EB,\u00E8,\u00EA,\u0119,\u0117,\u0113", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + /* morekeys_i */ "\u00ED,\u00EF,\u00EE,\u00EC,\u012F,\u012B", + /* keylabel_to_alpha */ null, + /* morekeys_c */ null, + /* double_quotes */ "!text/double_9qm_lqm", + /* morekeys_n */ null, + /* single_quotes */ "!text/single_9qm_lqm", + /* morekeys_s */ null, + // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE + // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS + /* morekeys_y */ "\u00FD,\u00FF", + // U+00F0: "ð" LATIN SMALL LETTER ETH + /* morekeys_d */ "\u00F0", + /* morekeys_z */ null, + /* keyspec_currency */ null, + // U+00FE: "þ" LATIN SMALL LETTER THORN + /* morekeys_t */ "\u00FE", + }; + + /* Locale it: Italian */ + private static final String[] TEXTS_it = { + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // U+00AA: "ª" FEMININE ORDINAL INDICATOR + /* morekeys_a */ "\u00E0,\u00E1,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101,\u00AA", + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + // U+00BA: "º" MASCULINE ORDINAL INDICATOR + /* morekeys_o */ "\u00F2,\u00F3,\u00F4,\u00F6,\u00F5,\u0153,\u00F8,\u014D,\u00BA", + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00F9,\u00FA,\u00FB,\u00FC,\u016B", + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* morekeys_e */ "\u00E8,\u00E9,\u00EA,\u00EB,\u0119,\u0117,\u0113", + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + /* morekeys_i */ "\u00EC,\u00ED,\u00EE,\u00EF,\u012F,\u012B", + /* keylabel_to_alpha ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, + /* ~ keyspec_tablet_comma */ + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + /* keyspec_swiss_row1_11 */ "\u00FC", + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + /* keyspec_swiss_row2_10 */ "\u00F6", + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + /* keyspec_swiss_row2_11 */ "\u00E4", + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + /* morekeys_swiss_row1_11 */ "\u00E8", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + /* morekeys_swiss_row2_10 */ "\u00E9", + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + /* morekeys_swiss_row2_11 */ "\u00E0", + }; + + /* Locale iw: Hebrew */ + private static final String[] TEXTS_iw = { + /* morekeys_a ~ */ + null, null, null, null, null, + /* ~ morekeys_i */ + // Label for "switch to alphabetic" key. + // U+05D0: "א" HEBREW LETTER ALEF + // U+05D1: "ב" HEBREW LETTER BET + // U+05D2: "ג" HEBREW LETTER GIMEL + /* keylabel_to_alpha */ "\u05D0\u05D1\u05D2", + /* morekeys_c */ null, + /* double_quotes */ "!text/double_rqm_9qm", + /* morekeys_n */ null, + /* single_quotes */ "!text/single_rqm_9qm", + /* morekeys_s ~ */ + null, null, null, null, + /* ~ morekeys_z */ + // U+20AA: "₪" NEW SHEQEL SIGN + /* keyspec_currency */ "\u20AA", + /* morekeys_t ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, + /* ~ morekeys_swiss_row2_11 */ + // U+2605: "★" BLACK STAR + /* morekeys_star */ "\u2605", + // The all letters need to be mirrored are found at + // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt + // U+2264: "≤" LESS-THAN OR EQUAL TO + // U+2265: "≥" GREATER-THAN EQUAL TO + // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK + // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + /* keyspec_left_parenthesis */ "(|)", + /* keyspec_right_parenthesis */ ")|(", + /* keyspec_left_square_bracket */ "[|]", + /* keyspec_right_square_bracket */ "]|[", + /* keyspec_left_curly_bracket */ "{|}", + /* keyspec_right_curly_bracket */ "}|{", + /* keyspec_less_than */ "<|>", + /* keyspec_greater_than */ ">|<", + /* keyspec_less_than_equal */ "\u2264|\u2265", + /* keyspec_greater_than_equal */ "\u2265|\u2264", + /* keyspec_left_double_angle_quote */ "\u00AB|\u00BB", + /* keyspec_right_double_angle_quote */ "\u00BB|\u00AB", + /* keyspec_left_single_angle_quote */ "\u2039|\u203A", + /* keyspec_right_single_angle_quote */ "\u203A|\u2039", + /* morekeys_tablet_comma ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_currency_dollar */ + // U+00B1: "±" PLUS-MINUS SIGN + // U+FB29: "﬩" HEBREW LETTER ALTERNATIVE PLUS SIGN + /* morekeys_plus */ "\u00B1,\uFB29", + }; + + /* Locale ka_GE: Georgian (Georgia) */ + private static final String[] TEXTS_ka_GE = { + /* morekeys_a ~ */ + null, null, null, null, null, + /* ~ morekeys_i */ + // Label for "switch to alphabetic" key. + // U+10D0: "ა" GEORGIAN LETTER AN + // U+10D1: "ბ" GEORGIAN LETTER BAN + // U+10D2: "გ" GEORGIAN LETTER GAN + /* keylabel_to_alpha */ "\u10D0\u10D1\u10D2", + /* morekeys_c */ null, + /* double_quotes */ "!text/double_9qm_lqm", + /* morekeys_n */ null, + /* single_quotes */ "!text/single_9qm_lqm", + }; + + /* Locale kk: Kazakh */ + private static final String[] TEXTS_kk = { + /* morekeys_a ~ */ + null, null, null, null, null, + /* ~ morekeys_i */ + // Label for "switch to alphabetic" key. + // U+0410: "А" CYRILLIC CAPITAL LETTER A + // U+0411: "Б" CYRILLIC CAPITAL LETTER BE + // U+0412: "В" CYRILLIC CAPITAL LETTER VE + /* keylabel_to_alpha */ "\u0410\u0411\u0412", + /* morekeys_c ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, + /* ~ morekeys_k */ + // U+0451: "ё" CYRILLIC SMALL LETTER IO + /* morekeys_cyrillic_ie */ "\u0451", + /* keyspec_nordic_row1_11 ~ */ + null, null, null, null, + /* ~ morekeys_nordic_row2_10 */ + // U+0449: "щ" CYRILLIC SMALL LETTER SHCHA + /* keyspec_east_slavic_row1_9 */ "\u0449", + // U+044B: "ы" CYRILLIC SMALL LETTER YERU + /* keyspec_east_slavic_row2_2 */ "\u044B", + // U+044D: "э" CYRILLIC SMALL LETTER E + /* keyspec_east_slavic_row2_11 */ "\u044D", + // U+0438: "и" CYRILLIC SMALL LETTER I + /* keyspec_east_slavic_row3_5 */ "\u0438", + // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN + /* morekeys_cyrillic_soft_sign */ "\u044A", + /* keyspec_symbols_1 ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, + /* ~ morekeys_w */ + // U+0456: "і" CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I + /* morekeys_east_slavic_row2_2 */ "\u0456", + // U+04AF: "ү" CYRILLIC SMALL LETTER STRAIGHT U + // U+04B1: "ұ" CYRILLIC SMALL LETTER STRAIGHT U WITH STROKE + /* morekeys_cyrillic_u */ "\u04AF,\u04B1", + // U+04A3: "ң" CYRILLIC SMALL LETTER EN WITH DESCENDER + /* morekeys_cyrillic_en */ "\u04A3", + // U+0493: "ғ" CYRILLIC SMALL LETTER GHE WITH STROKE + /* morekeys_cyrillic_ghe */ "\u0493", + // U+04E9: "ө" CYRILLIC SMALL LETTER BARRED O + /* morekeys_cyrillic_o */ "\u04E9", + /* morekeys_cyrillic_i ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ keyspec_x */ + // U+04BB: "һ" CYRILLIC SMALL LETTER SHHA + /* morekeys_east_slavic_row2_11 */ "\u04BB", + // U+049B: "қ" CYRILLIC SMALL LETTER KA WITH DESCENDER + /* morekeys_cyrillic_ka */ "\u049B", + // U+04D9: "ә" CYRILLIC SMALL LETTER SCHWA + /* morekeys_cyrillic_a */ "\u04D9", + }; + + /* Locale km_KH: Khmer (Cambodia) */ + private static final String[] TEXTS_km_KH = { + /* morekeys_a ~ */ + null, null, null, null, null, + /* ~ morekeys_i */ + // Label for "switch to alphabetic" key. + // U+1780: "ក" KHMER LETTER KA + // U+1781: "ខ" KHMER LETTER KHA + // U+1782: "គ" KHMER LETTER KO + /* keylabel_to_alpha */ "\u1780\u1781\u1782", + /* morekeys_c ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_cyrillic_a */ + // U+17DB: "៛" KHMER CURRENCY SYMBOL RIEL + /* morekeys_currency_dollar */ "\u17DB,\u00A2,\u00A3,\u20AC,\u00A5,\u20B1", + }; + + /* Locale ky: Kirghiz */ + private static final String[] TEXTS_ky = { + /* morekeys_a ~ */ + null, null, null, null, null, + /* ~ morekeys_i */ + // Label for "switch to alphabetic" key. + // U+0410: "А" CYRILLIC CAPITAL LETTER A + // U+0411: "Б" CYRILLIC CAPITAL LETTER BE + // U+0412: "В" CYRILLIC CAPITAL LETTER VE + /* keylabel_to_alpha */ "\u0410\u0411\u0412", + /* morekeys_c ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, + /* ~ morekeys_k */ + // U+0451: "ё" CYRILLIC SMALL LETTER IO + /* morekeys_cyrillic_ie */ "\u0451", + /* keyspec_nordic_row1_11 ~ */ + null, null, null, null, + /* ~ morekeys_nordic_row2_10 */ + // U+0449: "щ" CYRILLIC SMALL LETTER SHCHA + /* keyspec_east_slavic_row1_9 */ "\u0449", + // U+044B: "ы" CYRILLIC SMALL LETTER YERU + /* keyspec_east_slavic_row2_2 */ "\u044B", + // U+044D: "э" CYRILLIC SMALL LETTER E + /* keyspec_east_slavic_row2_11 */ "\u044D", + // U+0438: "и" CYRILLIC SMALL LETTER I + /* keyspec_east_slavic_row3_5 */ "\u0438", + // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN + /* morekeys_cyrillic_soft_sign */ "\u044A", + /* keyspec_symbols_1 ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, + /* ~ morekeys_east_slavic_row2_2 */ + // U+04AF: "ү" CYRILLIC SMALL LETTER STRAIGHT U + /* morekeys_cyrillic_u */ "\u04AF", + // U+04A3: "ң" CYRILLIC SMALL LETTER EN WITH DESCENDER + /* morekeys_cyrillic_en */ "\u04A3", + /* morekeys_cyrillic_ghe */ null, + // U+04E9: "ө" CYRILLIC SMALL LETTER BARRED O + /* morekeys_cyrillic_o */ "\u04E9", + }; + + /* Locale lo_LA: Lao (Laos) */ + private static final String[] TEXTS_lo_LA = { + /* morekeys_a ~ */ + null, null, null, null, null, + /* ~ morekeys_i */ + // Label for "switch to alphabetic" key. + // U+0E81: "ກ" LAO LETTER KO + // U+0E82: "ຂ" LAO LETTER KHO SUNG + // U+0E84: "ຄ" LAO LETTER KHO TAM + /* keylabel_to_alpha */ "\u0E81\u0E82\u0E84", + /* morekeys_c ~ */ + null, null, null, null, null, null, null, null, + /* ~ morekeys_z */ + // U+20AD: "₭" KIP SIGN + /* keyspec_currency */ "\u20AD", + }; + + /* Locale lt: Lithuanian */ + private static final String[] TEXTS_lt = { + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+00E6: "æ" LATIN SMALL LETTER AE + /* morekeys_a */ "\u0105,\u00E4,\u0101,\u00E0,\u00E1,\u00E2,\u00E3,\u00E5,\u00E6", + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + /* morekeys_o */ "\u00F6,\u00F5,\u00F2,\u00F3,\u00F4,\u0153,\u0151,\u00F8", + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE + // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE + /* morekeys_u */ "\u016B,\u0173,\u00FC,\u016B,\u00F9,\u00FA,\u00FB,\u016F,\u0171", + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+011B: "ě" LATIN SMALL LETTER E WITH CARON + /* morekeys_e */ "\u0117,\u0119,\u0113,\u00E8,\u00E9,\u00EA,\u00EB,\u011B", + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+0131: "ı" LATIN SMALL LETTER DOTLESS I + /* morekeys_i */ "\u012F,\u012B,\u00EC,\u00ED,\u00EE,\u00EF,\u0131", + /* keylabel_to_alpha */ null, + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + /* morekeys_c */ "\u010D,\u00E7,\u0107", + /* double_quotes */ "!text/double_9qm_lqm", + // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* morekeys_n */ "\u0146,\u00F1,\u0144", + /* single_quotes */ "!text/single_9qm_lqm", + // U+0161: "š" LATIN SMALL LETTER S WITH CARON + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE + // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA + /* morekeys_s */ "\u0161,\u00DF,\u015B,\u015F", + // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE + // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS + /* morekeys_y */ "\u00FD,\u00FF", + // U+010F: "ď" LATIN SMALL LETTER D WITH CARON + /* morekeys_d */ "\u010F", + // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON + // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE + // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE + /* morekeys_z */ "\u017E,\u017C,\u017A", + /* keyspec_currency */ null, + // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA + // U+0165: "ť" LATIN SMALL LETTER T WITH CARON + /* morekeys_t */ "\u0163,\u0165", + // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA + // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE + // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE + // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON + /* morekeys_l */ "\u013C,\u0142,\u013A,\u013E", + // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA + // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE + /* morekeys_g */ "\u0123,\u011F", + /* single_angle_quotes */ null, + /* double_angle_quotes */ null, + // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA + // U+0159: "ř" LATIN SMALL LETTER R WITH CARON + // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE + /* morekeys_r */ "\u0157,\u0159,\u0155", + // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA + /* morekeys_k */ "\u0137", + }; + + /* Locale lv: Latvian */ + private static final String[] TEXTS_lv = { + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + /* morekeys_a */ "\u0101,\u00E0,\u00E1,\u00E2,\u00E3,\u00E4,\u00E5,\u00E6,\u0105", + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + /* morekeys_o */ "\u00F2,\u00F3,\u00F4,\u00F5,\u00F6,\u0153,\u0151,\u00F8", + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE + // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE + /* morekeys_u */ "\u016B,\u0173,\u00F9,\u00FA,\u00FB,\u00FC,\u016F,\u0171", + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+011B: "ě" LATIN SMALL LETTER E WITH CARON + /* morekeys_e */ "\u0113,\u0117,\u00E8,\u00E9,\u00EA,\u00EB,\u0119,\u011B", + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+0131: "ı" LATIN SMALL LETTER DOTLESS I + /* morekeys_i */ "\u012B,\u012F,\u00EC,\u00ED,\u00EE,\u00EF,\u0131", + /* keylabel_to_alpha */ null, + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + /* morekeys_c */ "\u010D,\u00E7,\u0107", + /* double_quotes */ "!text/double_9qm_lqm", + // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* morekeys_n */ "\u0146,\u00F1,\u0144", + /* single_quotes */ "!text/single_9qm_lqm", + // U+0161: "š" LATIN SMALL LETTER S WITH CARON + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE + // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA + /* morekeys_s */ "\u0161,\u00DF,\u015B,\u015F", + // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE + // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS + /* morekeys_y */ "\u00FD,\u00FF", + // U+010F: "ď" LATIN SMALL LETTER D WITH CARON + /* morekeys_d */ "\u010F", + // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON + // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE + // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE + /* morekeys_z */ "\u017E,\u017C,\u017A", + /* keyspec_currency */ null, + // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA + // U+0165: "ť" LATIN SMALL LETTER T WITH CARON + /* morekeys_t */ "\u0163,\u0165", + // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA + // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE + // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE + // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON + /* morekeys_l */ "\u013C,\u0142,\u013A,\u013E", + // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA + // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE + /* morekeys_g */ "\u0123,\u011F", + /* single_angle_quotes */ null, + /* double_angle_quotes */ null, + // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA + // U+0159: "ř" LATIN SMALL LETTER R WITH CARON + // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE + /* morekeys_r */ "\u0157,\u0159,\u0155", + // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA + /* morekeys_k */ "\u0137", + }; + + /* Locale mk: Macedonian */ + private static final String[] TEXTS_mk = { + /* morekeys_a ~ */ + null, null, null, null, null, + /* ~ morekeys_i */ + // Label for "switch to alphabetic" key. + // U+0410: "А" CYRILLIC CAPITAL LETTER A + // U+0411: "Б" CYRILLIC CAPITAL LETTER BE + // U+0412: "В" CYRILLIC CAPITAL LETTER VE + /* keylabel_to_alpha */ "\u0410\u0411\u0412", + /* morekeys_c */ null, + /* double_quotes */ "!text/double_9qm_lqm", + /* morekeys_n */ null, + /* single_quotes */ "!text/single_9qm_lqm", + /* morekeys_s ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_k */ + // U+0450: "ѐ" CYRILLIC SMALL LETTER IE WITH GRAVE + /* morekeys_cyrillic_ie */ "\u0450", + /* keyspec_nordic_row1_11 ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, + /* ~ morekeys_cyrillic_o */ + // U+045D: "ѝ" CYRILLIC SMALL LETTER I WITH GRAVE + /* morekeys_cyrillic_i */ "\u045D", + // U+0455: "ѕ" CYRILLIC SMALL LETTER DZE + /* keyspec_south_slavic_row1_6 */ "\u0455", + // U+045C: "ќ" CYRILLIC SMALL LETTER KJE + /* keyspec_south_slavic_row2_11 */ "\u045C", + // U+0437: "з" CYRILLIC SMALL LETTER ZE + /* keyspec_south_slavic_row3_1 */ "\u0437", + // U+0453: "ѓ" CYRILLIC SMALL LETTER GJE + /* keyspec_south_slavic_row3_8 */ "\u0453", + }; + + /* Locale mn_MN: Mongolian (Mongolia) */ + private static final String[] TEXTS_mn_MN = { + /* morekeys_a ~ */ + null, null, null, null, null, + /* ~ morekeys_i */ + // Label for "switch to alphabetic" key. + // U+0410: "А" CYRILLIC CAPITAL LETTER A + // U+0411: "Б" CYRILLIC CAPITAL LETTER BE + // U+0412: "В" CYRILLIC CAPITAL LETTER VE + /* keylabel_to_alpha */ "\u0410\u0411\u0412", + /* morekeys_c ~ */ + null, null, null, null, null, null, null, null, + /* ~ morekeys_z */ + // U+20AE: "₮" TUGRIK SIGN + /* keyspec_currency */ "\u20AE", + }; + + /* Locale mr_IN: Marathi (India) */ + private static final String[] TEXTS_mr_IN = { + /* morekeys_a ~ */ + null, null, null, null, null, + /* ~ morekeys_i */ + // Label for "switch to alphabetic" key. + // U+0915: "क" DEVANAGARI LETTER KA + // U+0916: "ख" DEVANAGARI LETTER KHA + // U+0917: "ग" DEVANAGARI LETTER GA + /* keylabel_to_alpha */ "\u0915\u0916\u0917", + /* morekeys_c ~ */ + null, null, null, null, null, null, null, null, + /* ~ morekeys_z */ + // U+20B9: "₹" INDIAN RUPEE SIGN + /* keyspec_currency */ "\u20B9", + /* morekeys_t ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, + /* ~ morekeys_cyrillic_soft_sign */ + // U+0967: "१" DEVANAGARI DIGIT ONE + /* keyspec_symbols_1 */ "\u0967", + // U+0968: "२" DEVANAGARI DIGIT TWO + /* keyspec_symbols_2 */ "\u0968", + // U+0969: "३" DEVANAGARI DIGIT THREE + /* keyspec_symbols_3 */ "\u0969", + // U+096A: "४" DEVANAGARI DIGIT FOUR + /* keyspec_symbols_4 */ "\u096A", + // U+096B: "५" DEVANAGARI DIGIT FIVE + /* keyspec_symbols_5 */ "\u096B", + // U+096C: "६" DEVANAGARI DIGIT SIX + /* keyspec_symbols_6 */ "\u096C", + // U+096D: "७" DEVANAGARI DIGIT SEVEN + /* keyspec_symbols_7 */ "\u096D", + // U+096E: "८" DEVANAGARI DIGIT EIGHT + /* keyspec_symbols_8 */ "\u096E", + // U+096F: "९" DEVANAGARI DIGIT NINE + /* keyspec_symbols_9 */ "\u096F", + // U+0966: "०" DEVANAGARI DIGIT ZERO + /* keyspec_symbols_0 */ "\u0966", + // Label for "switch to symbols" key. + /* keylabel_to_symbol */ "?\u0967\u0968\u0969", + /* additional_morekeys_symbols_1 */ "1", + /* additional_morekeys_symbols_2 */ "2", + /* additional_morekeys_symbols_3 */ "3", + /* additional_morekeys_symbols_4 */ "4", + /* additional_morekeys_symbols_5 */ "5", + /* additional_morekeys_symbols_6 */ "6", + /* additional_morekeys_symbols_7 */ "7", + /* additional_morekeys_symbols_8 */ "8", + /* additional_morekeys_symbols_9 */ "9", + /* additional_morekeys_symbols_0 */ "0", + }; + + /* Locale my_MM: Burmese (Myanmar) */ + private static final String[] TEXTS_my_MM = { + /* morekeys_a ~ */ + null, null, null, null, null, + /* ~ morekeys_i */ + // Label for "switch to alphabetic" key. + // U+1000: "က" MYANMAR LETTER KA + // U+1001: "ခ" MYANMAR LETTER KHA + // U+1002: "ဂ" MYANMAR LETTER GA + /* keylabel_to_alpha */ "\u1000\u1001\u1002", + /* morekeys_c ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, + /* ~ morekeys_nordic_row2_11 */ + /* morekeys_punctuation */ "!autoColumnOrder!9,\u104A,.,?,!,#,),(,/,;,...,',@,:,-,\",+,\\%,&", + // U+104A: "၊" MYANMAR SIGN LITTLE SECTION + // U+104B: "။" MYANMAR SIGN SECTION + /* keyspec_tablet_comma */ "\u104A", + /* keyspec_swiss_row1_11 ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, + /* ~ keyspec_right_single_angle_quote */ + /* morekeys_tablet_comma */ "\\,", + /* keyhintlabel_period */ "\u104A", + /* morekeys_tablet_period ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ keyspec_south_slavic_row3_8 */ + /* morekeys_tablet_punctuation */ "!autoColumnOrder!8,.,',#,),(,/,;,@,...,:,-,\",+,\\%,&", + /* keyspec_spanish_row2_10 ~ */ + null, null, null, null, null, null, null, + /* ~ keyhintlabel_tablet_comma */ + /* keyspec_period */ "\u104B", + /* morekeys_period */ null, + /* keyspec_tablet_period */ "\u104B", + }; + + /* Locale nb: Norwegian Bokmål */ + private static final String[] TEXTS_nb = { + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + /* morekeys_a */ "\u00E0,\u00E4,\u00E1,\u00E2,\u00E3,\u0101", + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + /* morekeys_o */ "\u00F4,\u00F2,\u00F3,\u00F6,\u00F5,\u0153,\u014D", + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FC,\u00FB,\u00F9,\u00FA,\u016B", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* morekeys_e */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113", + /* morekeys_i ~ */ + null, null, null, + /* ~ morekeys_c */ + /* double_quotes */ "!text/double_9qm_rqm", + /* morekeys_n */ null, + /* single_quotes */ "!text/single_9qm_rqm", + /* morekeys_s ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_cyrillic_ie */ + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + /* keyspec_nordic_row1_11 */ "\u00E5", + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + /* keyspec_nordic_row2_10 */ "\u00F8", + // U+00E6: "æ" LATIN SMALL LETTER AE + /* keyspec_nordic_row2_11 */ "\u00E6", + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + /* morekeys_nordic_row2_10 */ "\u00F6", + /* keyspec_east_slavic_row1_9 ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, + /* ~ additional_morekeys_symbols_0 */ + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + /* morekeys_nordic_row2_11 */ "\u00E4", + }; + + /* Locale ne_NP: Nepali (Nepal) */ + private static final String[] TEXTS_ne_NP = { + /* morekeys_a ~ */ + null, null, null, null, null, + /* ~ morekeys_i */ + // Label for "switch to alphabetic" key. + // U+0915: "क" DEVANAGARI LETTER KA + // U+0916: "ख" DEVANAGARI LETTER KHA + // U+0917: "ग" DEVANAGARI LETTER GA + /* keylabel_to_alpha */ "\u0915\u0916\u0917", + /* morekeys_c ~ */ + null, null, null, null, null, null, null, null, + /* ~ morekeys_z */ + // U+0930/U+0941/U+002E "रु." NEPALESE RUPEE SIGN + /* keyspec_currency */ "\u0930\u0941.", + /* morekeys_t ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, + /* ~ morekeys_cyrillic_soft_sign */ + // U+0967: "१" DEVANAGARI DIGIT ONE + /* keyspec_symbols_1 */ "\u0967", + // U+0968: "२" DEVANAGARI DIGIT TWO + /* keyspec_symbols_2 */ "\u0968", + // U+0969: "३" DEVANAGARI DIGIT THREE + /* keyspec_symbols_3 */ "\u0969", + // U+096A: "४" DEVANAGARI DIGIT FOUR + /* keyspec_symbols_4 */ "\u096A", + // U+096B: "५" DEVANAGARI DIGIT FIVE + /* keyspec_symbols_5 */ "\u096B", + // U+096C: "६" DEVANAGARI DIGIT SIX + /* keyspec_symbols_6 */ "\u096C", + // U+096D: "७" DEVANAGARI DIGIT SEVEN + /* keyspec_symbols_7 */ "\u096D", + // U+096E: "८" DEVANAGARI DIGIT EIGHT + /* keyspec_symbols_8 */ "\u096E", + // U+096F: "९" DEVANAGARI DIGIT NINE + /* keyspec_symbols_9 */ "\u096F", + // U+0966: "०" DEVANAGARI DIGIT ZERO + /* keyspec_symbols_0 */ "\u0966", + // Label for "switch to symbols" key. + /* keylabel_to_symbol */ "?\u0967\u0968\u0969", + /* additional_morekeys_symbols_1 */ "1", + /* additional_morekeys_symbols_2 */ "2", + /* additional_morekeys_symbols_3 */ "3", + /* additional_morekeys_symbols_4 */ "4", + /* additional_morekeys_symbols_5 */ "5", + /* additional_morekeys_symbols_6 */ "6", + /* additional_morekeys_symbols_7 */ "7", + /* additional_morekeys_symbols_8 */ "8", + /* additional_morekeys_symbols_9 */ "9", + /* additional_morekeys_symbols_0 */ "0", + }; + + /* Locale nl: Dutch */ + private static final String[] TEXTS_nl = { + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + /* morekeys_a */ "\u00E1,\u00E4,\u00E2,\u00E0,\u00E6,\u00E3,\u00E5,\u0101", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + /* morekeys_o */ "\u00F3,\u00F6,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FA,\u00FC,\u00FB,\u00F9,\u016B", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* morekeys_e */ "\u00E9,\u00EB,\u00EA,\u00E8,\u0119,\u0117,\u0113", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + // U+0133: "ij" LATIN SMALL LIGATURE IJ + /* morekeys_i */ "\u00ED,\u00EF,\u00EC,\u00EE,\u012F,\u012B,\u0133", + /* keylabel_to_alpha */ null, + /* morekeys_c */ null, + /* double_quotes */ "!text/double_9qm_rqm", + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* morekeys_n */ "\u00F1,\u0144", + /* single_quotes */ "!text/single_9qm_rqm", + /* morekeys_s */ null, + // U+0133: "ij" LATIN SMALL LIGATURE IJ + /* morekeys_y */ "\u0133", + }; + + /* Locale pl: Polish */ + private static final String[] TEXTS_pl = { + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + /* morekeys_a */ "\u0105,\u00E1,\u00E0,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + /* morekeys_o */ "\u00F3,\u00F6,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D", + /* morekeys_u */ null, + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* morekeys_e */ "\u0119,\u00E8,\u00E9,\u00EA,\u00EB,\u0117,\u0113", + /* morekeys_i */ null, + /* keylabel_to_alpha */ null, + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + /* morekeys_c */ "\u0107,\u00E7,\u010D", + /* double_quotes */ "!text/double_9qm_rqm", + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + /* morekeys_n */ "\u0144,\u00F1", + /* single_quotes */ "!text/single_9qm_rqm", + // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + // U+0161: "š" LATIN SMALL LETTER S WITH CARON + /* morekeys_s */ "\u015B,\u00DF,\u0161", + /* morekeys_y */ null, + /* morekeys_d */ null, + // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE + // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE + // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON + /* morekeys_z */ "\u017C,\u017A,\u017E", + /* keyspec_currency */ null, + /* morekeys_t */ null, + // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE + /* morekeys_l */ "\u0142", + }; + + /* Locale pt: Portuguese */ + private static final String[] TEXTS_pt = { + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00AA: "ª" FEMININE ORDINAL INDICATOR + /* morekeys_a */ "\u00E1,\u00E3,\u00E0,\u00E2,\u00E4,\u00E5,\u00E6,\u00AA", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + // U+00BA: "º" MASCULINE ORDINAL INDICATOR + /* morekeys_o */ "\u00F3,\u00F5,\u00F4,\u00F2,\u00F6,\u0153,\u00F8,\u014D,\u00BA", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + /* morekeys_e */ "\u00E9,\u00EA,\u00E8,\u0119,\u0117,\u0113,\u00EB", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + /* morekeys_i */ "\u00ED,\u00EE,\u00EC,\u00EF,\u012F,\u012B", + /* keylabel_to_alpha */ null, + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + /* morekeys_c */ "\u00E7,\u010D,\u0107", + }; + + /* Locale rm: Raeto-Romance */ + private static final String[] TEXTS_rm = { + /* morekeys_a */ null, + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + /* morekeys_o */ "\u00F2,\u00F3,\u00F6,\u00F4,\u00F5,\u0153,\u00F8", + }; + + /* Locale ro: Romanian */ + private static final String[] TEXTS_ro = { + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+0103: "ă" LATIN SMALL LETTER A WITH BREVE + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + /* morekeys_a */ "\u00E2,\u00E3,\u0103,\u00E0,\u00E1,\u00E4,\u00E6,\u00E5,\u0101", + /* morekeys_o ~ */ + null, null, null, + /* ~ morekeys_e */ + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + /* morekeys_i */ "\u00EE,\u00EF,\u00EC,\u00ED,\u012F,\u012B", + /* keylabel_to_alpha */ null, + /* morekeys_c */ null, + /* double_quotes */ "!text/double_9qm_rqm", + /* morekeys_n */ null, + /* single_quotes */ "!text/single_9qm_rqm", + // U+0219: "ș" LATIN SMALL LETTER S WITH COMMA BELOW + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE + // U+0161: "š" LATIN SMALL LETTER S WITH CARON + /* morekeys_s */ "\u0219,\u00DF,\u015B,\u0161", + /* morekeys_y ~ */ + null, null, null, null, + /* ~ keyspec_currency */ + // U+021B: "ț" LATIN SMALL LETTER T WITH COMMA BELOW + /* morekeys_t */ "\u021B", + }; + + /* Locale ru: Russian */ + private static final String[] TEXTS_ru = { + /* morekeys_a ~ */ + null, null, null, null, null, + /* ~ morekeys_i */ + // Label for "switch to alphabetic" key. + // U+0410: "А" CYRILLIC CAPITAL LETTER A + // U+0411: "Б" CYRILLIC CAPITAL LETTER BE + // U+0412: "В" CYRILLIC CAPITAL LETTER VE + /* keylabel_to_alpha */ "\u0410\u0411\u0412", + /* morekeys_c */ null, + /* double_quotes */ "!text/double_9qm_lqm", + /* morekeys_n */ null, + /* single_quotes */ "!text/single_9qm_lqm", + /* morekeys_s ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_k */ + // U+0451: "ё" CYRILLIC SMALL LETTER IO + /* morekeys_cyrillic_ie */ "\u0451", + /* keyspec_nordic_row1_11 ~ */ + null, null, null, null, + /* ~ morekeys_nordic_row2_10 */ + // U+0449: "щ" CYRILLIC SMALL LETTER SHCHA + /* keyspec_east_slavic_row1_9 */ "\u0449", + // U+044B: "ы" CYRILLIC SMALL LETTER YERU + /* keyspec_east_slavic_row2_2 */ "\u044B", + // U+044D: "э" CYRILLIC SMALL LETTER E + /* keyspec_east_slavic_row2_11 */ "\u044D", + // U+0438: "и" CYRILLIC SMALL LETTER I + /* keyspec_east_slavic_row3_5 */ "\u0438", + // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN + /* morekeys_cyrillic_soft_sign */ "\u044A", + }; + + /* Locale si_LK: Sinhalese (Sri Lanka) */ + private static final String[] TEXTS_si_LK = { + /* morekeys_a ~ */ + null, null, null, null, null, + /* ~ morekeys_i */ + // Label for "switch to alphabetic" key. + // U+0D85: "අ" SINHALA LETTER AYANNA + // U+0D86: "ආ" SINHALA LETTER AAYANNA + /* keylabel_to_alpha */ "\u0D85,\u0D86", + }; + + /* Locale sk: Slovak */ + private static final String[] TEXTS_sk = { + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + /* morekeys_a */ "\u00E1,\u00E4,\u0101,\u00E0,\u00E2,\u00E3,\u00E5,\u00E6,\u0105", + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + /* morekeys_o */ "\u00F4,\u00F3,\u00F6,\u00F2,\u00F5,\u0153,\u0151,\u00F8", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE + /* morekeys_u */ "\u00FA,\u016F,\u00FC,\u016B,\u0173,\u00F9,\u00FB,\u0171", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+011B: "ě" LATIN SMALL LETTER E WITH CARON + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + /* morekeys_e */ "\u00E9,\u011B,\u0113,\u0117,\u00E8,\u00EA,\u00EB,\u0119", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+0131: "ı" LATIN SMALL LETTER DOTLESS I + /* morekeys_i */ "\u00ED,\u012B,\u012F,\u00EC,\u00EE,\u00EF,\u0131", + /* keylabel_to_alpha */ null, + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + /* morekeys_c */ "\u010D,\u00E7,\u0107", + /* double_quotes */ "!text/double_9qm_lqm", + // U+0148: "ň" LATIN SMALL LETTER N WITH CARON + // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* morekeys_n */ "\u0148,\u0146,\u00F1,\u0144", + /* single_quotes */ "!text/single_9qm_lqm", + // U+0161: "š" LATIN SMALL LETTER S WITH CARON + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE + // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA + /* morekeys_s */ "\u0161,\u00DF,\u015B,\u015F", + // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE + // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS + /* morekeys_y */ "\u00FD,\u00FF", + // U+010F: "ď" LATIN SMALL LETTER D WITH CARON + /* morekeys_d */ "\u010F", + // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON + // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE + // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE + /* morekeys_z */ "\u017E,\u017C,\u017A", + /* keyspec_currency */ null, + // U+0165: "ť" LATIN SMALL LETTER T WITH CARON + // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA + /* morekeys_t */ "\u0165,\u0163", + // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON + // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE + // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA + // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE + /* morekeys_l */ "\u013E,\u013A,\u013C,\u0142", + // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA + // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE + /* morekeys_g */ "\u0123,\u011F", + /* single_angle_quotes */ "!text/single_raqm_laqm", + /* double_angle_quotes */ "!text/double_raqm_laqm", + // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE + // U+0159: "ř" LATIN SMALL LETTER R WITH CARON + // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA + /* morekeys_r */ "\u0155,\u0159,\u0157", + // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA + /* morekeys_k */ "\u0137", + }; + + /* Locale sl: Slovenian */ + private static final String[] TEXTS_sl = { + /* morekeys_a ~ */ + null, null, null, null, null, null, + /* ~ keylabel_to_alpha */ + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + /* morekeys_c */ "\u010D,\u0107", + /* double_quotes */ "!text/double_9qm_lqm", + /* morekeys_n */ null, + /* single_quotes */ "!text/single_9qm_lqm", + // U+0161: "š" LATIN SMALL LETTER S WITH CARON + /* morekeys_s */ "\u0161", + /* morekeys_y */ null, + // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE + /* morekeys_d */ "\u0111", + // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON + /* morekeys_z */ "\u017E", + /* keyspec_currency ~ */ + null, null, null, null, + /* ~ morekeys_g */ + /* single_angle_quotes */ "!text/single_raqm_laqm", + /* double_angle_quotes */ "!text/double_raqm_laqm", + }; + + /* Locale sr: Serbian */ + private static final String[] TEXTS_sr = { + /* morekeys_a ~ */ + null, null, null, null, null, + /* ~ morekeys_i */ + // END: More keys definitions for Serbian (Cyrillic) + // Label for "switch to alphabetic" key. + // U+0410: "А" CYRILLIC CAPITAL LETTER A + // U+0411: "Б" CYRILLIC CAPITAL LETTER BE + // U+0412: "В" CYRILLIC CAPITAL LETTER VE + /* keylabel_to_alpha */ "\u0410\u0411\u0412", + /* morekeys_c */ null, + /* double_quotes */ "!text/double_9qm_lqm", + /* morekeys_n */ null, + /* single_quotes */ "!text/single_9qm_lqm", + /* morekeys_s ~ */ + null, null, null, null, null, null, null, null, + /* ~ morekeys_g */ + /* single_angle_quotes */ "!text/single_raqm_laqm", + /* double_angle_quotes */ "!text/double_raqm_laqm", + /* morekeys_r */ null, + /* morekeys_k */ null, + // U+0450: "ѐ" CYRILLIC SMALL LETTER IE WITH GRAVE + /* morekeys_cyrillic_ie */ "\u0450", + /* keyspec_nordic_row1_11 ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, + /* ~ morekeys_cyrillic_o */ + // U+045D: "ѝ" CYRILLIC SMALL LETTER I WITH GRAVE + /* morekeys_cyrillic_i */ "\u045D", + // TODO: Move these to sr-Latn once we can handle IETF language tag with script name specified. + // BEGIN: More keys definitions for Serbian (Latin) + // U+0161: "š" LATIN SMALL LETTER S WITH CARON + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE + // <string name="morekeys_s">š,ß,ś</string> + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + // <string name="morekeys_c">č,ç,ć</string> + // U+010F: "ď" LATIN SMALL LETTER D WITH CARON + // <string name="morekeys_d">ď</string> + // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON + // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE + // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE + // <string name="morekeys_z">ž,ź,ż</string> + // END: More keys definitions for Serbian (Latin) + // BEGIN: More keys definitions for Serbian (Cyrillic) + // U+0437: "з" CYRILLIC SMALL LETTER ZE + /* keyspec_south_slavic_row1_6 */ "\u0437", + // U+045B: "ћ" CYRILLIC SMALL LETTER TSHE + /* keyspec_south_slavic_row2_11 */ "\u045B", + // U+0455: "ѕ" CYRILLIC SMALL LETTER DZE + /* keyspec_south_slavic_row3_1 */ "\u0455", + // U+0452: "ђ" CYRILLIC SMALL LETTER DJE + /* keyspec_south_slavic_row3_8 */ "\u0452", + }; + + /* Locale sv: Swedish */ + private static final String[] TEXTS_sv = { + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + /* morekeys_a */ "\u00E1,\u00E0,\u00E2,\u0105,\u00E3", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + /* morekeys_o */ "\u00F3,\u00F2,\u00F4,\u00F5,\u014D", + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FC,\u00FA,\u00F9,\u00FB,\u016B", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + /* morekeys_e */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0119", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + /* morekeys_i */ "\u00ED,\u00EC,\u00EE,\u00EF", + /* keylabel_to_alpha */ null, + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + /* morekeys_c */ "\u00E7,\u0107,\u010D", + /* double_quotes */ null, + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0148: "ň" LATIN SMALL LETTER N WITH CARON + /* morekeys_n */ "\u0144,\u00F1,\u0148", + /* single_quotes */ null, + // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE + // U+0161: "š" LATIN SMALL LETTER S WITH CARON + // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + /* morekeys_s */ "\u015B,\u0161,\u015F,\u00DF", + // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE + // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS + /* morekeys_y */ "\u00FD,\u00FF", + // U+00F0: "ð" LATIN SMALL LETTER ETH + // U+010F: "ď" LATIN SMALL LETTER D WITH CARON + /* morekeys_d */ "\u00F0,\u010F", + // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE + // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON + // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE + /* morekeys_z */ "\u017A,\u017E,\u017C", + /* keyspec_currency */ null, + // U+0165: "ť" LATIN SMALL LETTER T WITH CARON + // U+00FE: "þ" LATIN SMALL LETTER THORN + /* morekeys_t */ "\u0165,\u00FE", + // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE + /* morekeys_l */ "\u0142", + /* morekeys_g */ null, + /* single_angle_quotes */ "!text/single_raqm_laqm", + /* double_angle_quotes */ "!text/double_raqm_laqm", + // U+0159: "ř" LATIN SMALL LETTER R WITH CARON + /* morekeys_r */ "\u0159", + /* morekeys_k */ null, + /* morekeys_cyrillic_ie */ null, + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + /* keyspec_nordic_row1_11 */ "\u00E5", + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + /* keyspec_nordic_row2_10 */ "\u00F6", + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + /* keyspec_nordic_row2_11 */ "\u00E4", + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+0153: "œ" LATIN SMALL LIGATURE OE + /* morekeys_nordic_row2_10 */ "\u00F8,\u0153", + /* keyspec_east_slavic_row1_9 ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, + /* ~ additional_morekeys_symbols_0 */ + // U+00E6: "æ" LATIN SMALL LETTER AE + /* morekeys_nordic_row2_11 */ "\u00E6", + }; + + /* Locale sw: Swahili */ + private static final String[] TEXTS_sw = { + // This is the same as English except morekeys_g. + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + /* morekeys_a */ "\u00E0,\u00E1,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101", + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + /* morekeys_o */ "\u00F4,\u00F6,\u00F2,\u00F3,\u0153,\u00F8,\u014D,\u00F5", + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FB,\u00FC,\u00F9,\u00FA,\u016B", + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* morekeys_e */ "\u00E8,\u00E9,\u00EA,\u00EB,\u0113", + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + /* morekeys_i */ "\u00EE,\u00EF,\u00ED,\u012B,\u00EC", + /* keylabel_to_alpha */ null, + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + /* morekeys_c */ "\u00E7", + /* double_quotes */ null, + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + /* morekeys_n */ "\u00F1", + /* single_quotes */ null, + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + /* morekeys_s */ "\u00DF", + /* morekeys_y ~ */ + null, null, null, null, null, null, + /* ~ morekeys_l */ + /* morekeys_g */ "g\'", + }; + + /* Locale th: Thai */ + private static final String[] TEXTS_th = { + /* morekeys_a ~ */ + null, null, null, null, null, + /* ~ morekeys_i */ + // Label for "switch to alphabetic" key. + // U+0E01: "ก" THAI CHARACTER KO KAI + // U+0E02: "ข" THAI CHARACTER KHO KHAI + // U+0E04: "ค" THAI CHARACTER KHO KHWAI + /* keylabel_to_alpha */ "\u0E01\u0E02\u0E04", + /* morekeys_c ~ */ + null, null, null, null, null, null, null, null, + /* ~ morekeys_z */ + // U+0E3F: "฿" THAI CURRENCY SYMBOL BAHT + /* keyspec_currency */ "\u0E3F", + }; + + /* Locale tl: Tagalog */ + private static final String[] TEXTS_tl = { + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // U+00AA: "ª" FEMININE ORDINAL INDICATOR + /* morekeys_a */ "\u00E1,\u00E0,\u00E4,\u00E2,\u00E3,\u00E5,\u0105,\u00E6,\u0101,\u00AA", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + // U+00BA: "º" MASCULINE ORDINAL INDICATOR + /* morekeys_o */ "\u00F3,\u00F2,\u00F6,\u00F4,\u00F5,\u00F8,\u0153,\u014D,\u00BA", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* morekeys_e */ "\u00E9,\u00E8,\u00EB,\u00EA,\u0119,\u0117,\u0113", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + /* morekeys_i */ "\u00ED,\u00EF,\u00EC,\u00EE,\u012F,\u012B", + /* keylabel_to_alpha */ null, + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + /* morekeys_c */ "\u00E7,\u0107,\u010D", + /* double_quotes */ null, + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* morekeys_n */ "\u00F1,\u0144", + }; + + /* Locale tr: Turkish */ + private static final String[] TEXTS_tr = { + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + /* morekeys_a */ "\u00E2", + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + /* morekeys_o */ "\u00F6,\u00F4,\u0153,\u00F2,\u00F3,\u00F5,\u00F8,\u014D", + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FC,\u00FB,\u00F9,\u00FA,\u016B", + /* morekeys_e */ null, + // U+0131: "ı" LATIN SMALL LETTER DOTLESS I + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + /* morekeys_i */ "\u0131,\u00EE,\u00EF,\u00EC,\u00ED,\u012F,\u012B", + /* keylabel_to_alpha */ null, + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + /* morekeys_c */ "\u00E7,\u0107,\u010D", + /* double_quotes ~ */ + null, null, null, + /* ~ single_quotes */ + // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE + // U+0161: "š" LATIN SMALL LETTER S WITH CARON + /* morekeys_s */ "\u015F,\u00DF,\u015B,\u0161", + /* morekeys_y ~ */ + null, null, null, null, null, null, + /* ~ morekeys_l */ + // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE + /* morekeys_g */ "\u011F", + }; + + /* Locale uk: Ukrainian */ + private static final String[] TEXTS_uk = { + /* morekeys_a ~ */ + null, null, null, null, null, + /* ~ morekeys_i */ + // Label for "switch to alphabetic" key. + // U+0410: "А" CYRILLIC CAPITAL LETTER A + // U+0411: "Б" CYRILLIC CAPITAL LETTER BE + // U+0412: "В" CYRILLIC CAPITAL LETTER VE + /* keylabel_to_alpha */ "\u0410\u0411\u0412", + /* morekeys_c */ null, + /* double_quotes */ "!text/double_9qm_lqm", + /* morekeys_n */ null, + /* single_quotes */ "!text/single_9qm_lqm", + /* morekeys_s ~ */ + null, null, null, null, + /* ~ morekeys_z */ + // U+20B4: "₴" HRYVNIA SIGN + /* keyspec_currency */ "\u20B4", + /* morekeys_t ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_nordic_row2_10 */ + // U+0449: "щ" CYRILLIC SMALL LETTER SHCHA + /* keyspec_east_slavic_row1_9 */ "\u0449", + // U+0456: "і" CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I + /* keyspec_east_slavic_row2_2 */ "\u0456", + // U+0454: "є" CYRILLIC SMALL LETTER UKRAINIAN IE + /* keyspec_east_slavic_row2_11 */ "\u0454", + // U+0438: "и" CYRILLIC SMALL LETTER I + /* keyspec_east_slavic_row3_5 */ "\u0438", + // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN + /* morekeys_cyrillic_soft_sign */ "\u044A", + /* keyspec_symbols_1 ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, + /* ~ morekeys_w */ + // U+0457: "ї" CYRILLIC SMALL LETTER YI + /* morekeys_east_slavic_row2_2 */ "\u0457", + /* morekeys_cyrillic_u */ null, + /* morekeys_cyrillic_en */ null, + // U+0491: "ґ" CYRILLIC SMALL LETTER GHE WITH UPTURN + /* morekeys_cyrillic_ghe */ "\u0491", + }; + + /* Locale vi: Vietnamese */ + private static final String[] TEXTS_vi = { + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+1EA3: "ả" LATIN SMALL LETTER A WITH HOOK ABOVE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+1EA1: "ạ" LATIN SMALL LETTER A WITH DOT BELOW + // U+0103: "ă" LATIN SMALL LETTER A WITH BREVE + // U+1EB1: "ằ" LATIN SMALL LETTER A WITH BREVE AND GRAVE + // U+1EAF: "ắ" LATIN SMALL LETTER A WITH BREVE AND ACUTE + // U+1EB3: "ẳ" LATIN SMALL LETTER A WITH BREVE AND HOOK ABOVE + // U+1EB5: "ẵ" LATIN SMALL LETTER A WITH BREVE AND TILDE + // U+1EB7: "ặ" LATIN SMALL LETTER A WITH BREVE AND DOT BELOW + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+1EA7: "ầ" LATIN SMALL LETTER A WITH CIRCUMFLEX AND GRAVE + // U+1EA5: "ấ" LATIN SMALL LETTER A WITH CIRCUMFLEX AND ACUTE + // U+1EA9: "ẩ" LATIN SMALL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE + // U+1EAB: "ẫ" LATIN SMALL LETTER A WITH CIRCUMFLEX AND TILDE + // U+1EAD: "ậ" LATIN SMALL LETTER A WITH CIRCUMFLEX AND DOT BELOW + /* morekeys_a */ "\u00E0,\u00E1,\u1EA3,\u00E3,\u1EA1,\u0103,\u1EB1,\u1EAF,\u1EB3,\u1EB5,\u1EB7,\u00E2,\u1EA7,\u1EA5,\u1EA9,\u1EAB,\u1EAD", + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+1ECF: "ỏ" LATIN SMALL LETTER O WITH HOOK ABOVE + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+1ECD: "ọ" LATIN SMALL LETTER O WITH DOT BELOW + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+1ED3: "ồ" LATIN SMALL LETTER O WITH CIRCUMFLEX AND GRAVE + // U+1ED1: "ố" LATIN SMALL LETTER O WITH CIRCUMFLEX AND ACUTE + // U+1ED5: "ổ" LATIN SMALL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE + // U+1ED7: "ỗ" LATIN SMALL LETTER O WITH CIRCUMFLEX AND TILDE + // U+1ED9: "ộ" LATIN SMALL LETTER O WITH CIRCUMFLEX AND DOT BELOW + // U+01A1: "ơ" LATIN SMALL LETTER O WITH HORN + // U+1EDD: "ờ" LATIN SMALL LETTER O WITH HORN AND GRAVE + // U+1EDB: "ớ" LATIN SMALL LETTER O WITH HORN AND ACUTE + // U+1EDF: "ở" LATIN SMALL LETTER O WITH HORN AND HOOK ABOVE + // U+1EE1: "ỡ" LATIN SMALL LETTER O WITH HORN AND TILDE + // U+1EE3: "ợ" LATIN SMALL LETTER O WITH HORN AND DOT BELOW + /* morekeys_o */ "\u00F2,\u00F3,\u1ECF,\u00F5,\u1ECD,\u00F4,\u1ED3,\u1ED1,\u1ED5,\u1ED7,\u1ED9,\u01A1,\u1EDD,\u1EDB,\u1EDF,\u1EE1,\u1EE3", + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+1EE7: "ủ" LATIN SMALL LETTER U WITH HOOK ABOVE + // U+0169: "ũ" LATIN SMALL LETTER U WITH TILDE + // U+1EE5: "ụ" LATIN SMALL LETTER U WITH DOT BELOW + // U+01B0: "ư" LATIN SMALL LETTER U WITH HORN + // U+1EEB: "ừ" LATIN SMALL LETTER U WITH HORN AND GRAVE + // U+1EE9: "ứ" LATIN SMALL LETTER U WITH HORN AND ACUTE + // U+1EED: "ử" LATIN SMALL LETTER U WITH HORN AND HOOK ABOVE + // U+1EEF: "ữ" LATIN SMALL LETTER U WITH HORN AND TILDE + // U+1EF1: "ự" LATIN SMALL LETTER U WITH HORN AND DOT BELOW + /* morekeys_u */ "\u00F9,\u00FA,\u1EE7,\u0169,\u1EE5,\u01B0,\u1EEB,\u1EE9,\u1EED,\u1EEF,\u1EF1", + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+1EBB: "ẻ" LATIN SMALL LETTER E WITH HOOK ABOVE + // U+1EBD: "ẽ" LATIN SMALL LETTER E WITH TILDE + // U+1EB9: "ẹ" LATIN SMALL LETTER E WITH DOT BELOW + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+1EC1: "ề" LATIN SMALL LETTER E WITH CIRCUMFLEX AND GRAVE + // U+1EBF: "ế" LATIN SMALL LETTER E WITH CIRCUMFLEX AND ACUTE + // U+1EC3: "ể" LATIN SMALL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE + // U+1EC5: "ễ" LATIN SMALL LETTER E WITH CIRCUMFLEX AND TILDE + // U+1EC7: "ệ" LATIN SMALL LETTER E WITH CIRCUMFLEX AND DOT BELOW + /* morekeys_e */ "\u00E8,\u00E9,\u1EBB,\u1EBD,\u1EB9,\u00EA,\u1EC1,\u1EBF,\u1EC3,\u1EC5,\u1EC7", + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+1EC9: "ỉ" LATIN SMALL LETTER I WITH HOOK ABOVE + // U+0129: "ĩ" LATIN SMALL LETTER I WITH TILDE + // U+1ECB: "ị" LATIN SMALL LETTER I WITH DOT BELOW + /* morekeys_i */ "\u00EC,\u00ED,\u1EC9,\u0129,\u1ECB", + /* keylabel_to_alpha ~ */ + null, null, null, null, null, null, + /* ~ morekeys_s */ + // U+1EF3: "ỳ" LATIN SMALL LETTER Y WITH GRAVE + // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE + // U+1EF7: "ỷ" LATIN SMALL LETTER Y WITH HOOK ABOVE + // U+1EF9: "ỹ" LATIN SMALL LETTER Y WITH TILDE + // U+1EF5: "ỵ" LATIN SMALL LETTER Y WITH DOT BELOW + /* morekeys_y */ "\u1EF3,\u00FD,\u1EF7,\u1EF9,\u1EF5", + // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE + /* morekeys_d */ "\u0111", + /* morekeys_z */ null, + // U+20AB: "₫" DONG SIGN + /* keyspec_currency */ "\u20AB", + }; + + /* Locale zu: Zulu */ + private static final String[] TEXTS_zu = { + // This is the same as English + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + /* morekeys_a */ "\u00E0,\u00E1,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101", + // U+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+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + /* morekeys_o */ "\u00F3,\u00F4,\u00F6,\u00F2,\u0153,\u00F8,\u014D,\u00F5", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FA,\u00FB,\u00FC,\u00F9,\u016B", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* morekeys_e */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0113", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + /* morekeys_i */ "\u00ED,\u00EE,\u00EF,\u012B,\u00EC", + /* keylabel_to_alpha */ null, + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + /* morekeys_c */ "\u00E7", + /* double_quotes */ null, + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + /* morekeys_n */ "\u00F1", + /* single_quotes */ null, + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + /* morekeys_s */ "\u00DF", + }; + + /* Locale zz: Alphabet */ + private static final String[] TEXTS_zz = { + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // U+0103: "ă" LATIN SMALL LETTER A WITH BREVE + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + // U+00AA: "ª" FEMININE ORDINAL INDICATOR + /* morekeys_a */ "\u00E0,\u00E1,\u00E2,\u00E3,\u00E4,\u00E5,\u00E6,\u0101,\u0103,\u0105,\u00AA", + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + // U+014F: "ŏ" LATIN SMALL LETTER O WITH BREVE + // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00BA: "º" MASCULINE ORDINAL INDICATOR + /* morekeys_o */ "\u00F2,\u00F3,\u00F4,\u00F5,\u00F6,\u00F8,\u014D,\u014F,\u0151,\u0153,\u00BA", + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+0169: "ũ" LATIN SMALL LETTER U WITH TILDE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + // U+016D: "ŭ" LATIN SMALL LETTER U WITH BREVE + // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE + // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE + // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK + /* morekeys_u */ "\u00F9,\u00FA,\u00FB,\u00FC,\u0169,\u016B,\u016D,\u016F,\u0171,\u0173", + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + // U+0115: "ĕ" LATIN SMALL LETTER E WITH BREVE + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+011B: "ě" LATIN SMALL LETTER E WITH CARON + /* morekeys_e */ "\u00E8,\u00E9,\u00EA,\u00EB,\u0113,\u0115,\u0117,\u0119,\u011B", + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+0129: "ĩ" LATIN SMALL LETTER I WITH TILDE + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + // U+012D: "ĭ" LATIN SMALL LETTER I WITH BREVE + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+0131: "ı" LATIN SMALL LETTER DOTLESS I + // U+0133: "ij" LATIN SMALL LIGATURE IJ + /* morekeys_i */ "\u00EC,\u00ED,\u00EE,\u00EF,\u0129,\u012B,\u012D,\u012F,\u0131,\u0133", + /* keylabel_to_alpha */ null, + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + // U+0109: "ĉ" LATIN SMALL LETTER C WITH CIRCUMFLEX + // U+010B: "ċ" LATIN SMALL LETTER C WITH DOT ABOVE + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + /* morekeys_c */ "\u00E7,\u0107,\u0109,\u010B,\u010D", + /* double_quotes */ null, + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA + // U+0148: "ň" LATIN SMALL LETTER N WITH CARON + // U+0149: "ʼn" LATIN SMALL LETTER N PRECEDED BY APOSTROPHE + // U+014B: "ŋ" LATIN SMALL LETTER ENG + /* morekeys_n */ "\u00F1,\u0144,\u0146,\u0148,\u0149,\u014B", + /* single_quotes */ null, + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE + // U+015D: "ŝ" LATIN SMALL LETTER S WITH CIRCUMFLEX + // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA + // U+0161: "š" LATIN SMALL LETTER S WITH CARON + // U+017F: "ſ" LATIN SMALL LETTER LONG S + /* morekeys_s */ "\u00DF,\u015B,\u015D,\u015F,\u0161,\u017F", + // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE + // U+0177: "ŷ" LATIN SMALL LETTER Y WITH CIRCUMFLEX + // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS + // U+0133: "ij" LATIN SMALL LIGATURE IJ + /* morekeys_y */ "\u00FD,\u0177,\u00FF,\u0133", + // U+010F: "ď" LATIN SMALL LETTER D WITH CARON + // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE + // U+00F0: "ð" LATIN SMALL LETTER ETH + /* morekeys_d */ "\u010F,\u0111,\u00F0", + // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE + // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE + // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON + /* morekeys_z */ "\u017A,\u017C,\u017E", + /* keyspec_currency */ null, + // U+00FE: "þ" LATIN SMALL LETTER THORN + // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA + // U+0165: "ť" LATIN SMALL LETTER T WITH CARON + // U+0167: "ŧ" LATIN SMALL LETTER T WITH STROKE + /* morekeys_t */ "\u00FE,\u0163,\u0165,\u0167", + // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE + // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA + // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON + // U+0140: "ŀ" LATIN SMALL LETTER L WITH MIDDLE DOT + // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE + /* morekeys_l */ "\u013A,\u013C,\u013E,\u0140,\u0142", + // U+011D: "ĝ" LATIN SMALL LETTER G WITH CIRCUMFLEX + // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE + // U+0121: "ġ" LATIN SMALL LETTER G WITH DOT ABOVE + // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA + /* morekeys_g */ "\u011D,\u011F,\u0121,\u0123", + /* single_angle_quotes */ null, + /* double_angle_quotes */ null, + // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE + // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA + // U+0159: "ř" LATIN SMALL LETTER R WITH CARON + /* morekeys_r */ "\u0155,\u0157,\u0159", + // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA + // U+0138: "ĸ" LATIN SMALL LETTER KRA + /* morekeys_k */ "\u0137,\u0138", + /* morekeys_cyrillic_ie ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_question */ + // U+0125: "ĥ" LATIN SMALL LETTER H WITH CIRCUMFLEX + /* morekeys_h */ "\u0125", + // U+0175: "ŵ" LATIN SMALL LETTER W WITH CIRCUMFLEX + /* morekeys_w */ "\u0175", + /* morekeys_east_slavic_row2_2 ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_v */ + // U+0135: "ĵ" LATIN SMALL LETTER J WITH CIRCUMFLEX + /* morekeys_j */ "\u0135", + }; + + private static final Object[] LOCALES_AND_TEXTS = { + // "locale", TEXT_ARRAY, /* numberOfNonNullText/lengthOf_TEXT_ARRAY localeName */ + "DEFAULT", TEXTS_DEFAULT, /* 168/168 DEFAULT */ + "af" , TEXTS_af, /* 7/ 12 Afrikaans */ + "ar" , TEXTS_ar, /* 55/110 Arabic */ + "az_AZ" , TEXTS_az_AZ, /* 8/ 18 Azerbaijani (Azerbaijan) */ + "be_BY" , TEXTS_be_BY, /* 9/ 32 Belarusian (Belarus) */ + "bg" , TEXTS_bg, /* 2/ 8 Bulgarian */ + "bn" , TEXTS_bn, /* 2/ 15 Bengali */ + "ca" , TEXTS_ca, /* 11/ 95 Catalan */ + "cs" , TEXTS_cs, /* 17/ 21 Czech */ + "da" , TEXTS_da, /* 19/ 54 Danish */ + "de" , TEXTS_de, /* 16/ 62 German */ + "el" , TEXTS_el, /* 1/ 6 Greek */ + "en" , TEXTS_en, /* 8/ 11 English */ + "eo" , TEXTS_eo, /* 26/118 Esperanto */ + "es" , TEXTS_es, /* 8/ 55 Spanish */ + "et_EE" , TEXTS_et_EE, /* 22/ 27 Estonian (Estonia) */ + "eu_ES" , TEXTS_eu_ES, /* 7/ 9 Basque (Spain) */ + "fa" , TEXTS_fa, /* 58/125 Persian */ + "fi" , TEXTS_fi, /* 10/ 54 Finnish */ + "fr" , TEXTS_fr, /* 13/ 62 French */ + "gl_ES" , TEXTS_gl_ES, /* 7/ 9 Gallegan (Spain) */ + "hi" , TEXTS_hi, /* 23/ 53 Hindi */ + "hr" , TEXTS_hr, /* 9/ 20 Croatian */ + "hu" , TEXTS_hu, /* 9/ 20 Hungarian */ + "hy_AM" , TEXTS_hy_AM, /* 8/126 Armenian (Armenia) */ + "is" , TEXTS_is, /* 10/ 16 Icelandic */ + "it" , TEXTS_it, /* 11/ 62 Italian */ + "iw" , TEXTS_iw, /* 20/123 Hebrew */ + "ka_GE" , TEXTS_ka_GE, /* 3/ 10 Georgian (Georgia) */ + "kk" , TEXTS_kk, /* 15/121 Kazakh */ + "km_KH" , TEXTS_km_KH, /* 2/122 Khmer (Cambodia) */ + "ky" , TEXTS_ky, /* 10/ 88 Kirghiz */ + "lo_LA" , TEXTS_lo_LA, /* 2/ 15 Lao (Laos) */ + "lt" , TEXTS_lt, /* 18/ 22 Lithuanian */ + "lv" , TEXTS_lv, /* 18/ 22 Latvian */ + "mk" , TEXTS_mk, /* 9/ 93 Macedonian */ + "mn_MN" , TEXTS_mn_MN, /* 2/ 15 Mongolian (Mongolia) */ + "mr_IN" , TEXTS_mr_IN, /* 23/ 53 Marathi (India) */ + "my_MM" , TEXTS_my_MM, /* 8/104 Burmese (Myanmar) */ + "nb" , TEXTS_nb, /* 11/ 54 Norwegian Bokmål */ + "ne_NP" , TEXTS_ne_NP, /* 23/ 53 Nepali (Nepal) */ + "nl" , TEXTS_nl, /* 9/ 12 Dutch */ + "pl" , TEXTS_pl, /* 10/ 17 Polish */ + "pt" , TEXTS_pt, /* 6/ 7 Portuguese */ + "rm" , TEXTS_rm, /* 1/ 2 Raeto-Romance */ + "ro" , TEXTS_ro, /* 6/ 16 Romanian */ + "ru" , TEXTS_ru, /* 9/ 32 Russian */ + "si_LK" , TEXTS_si_LK, /* 1/ 6 Sinhalese (Sri Lanka) */ + "sk" , TEXTS_sk, /* 20/ 22 Slovak */ + "sl" , TEXTS_sl, /* 8/ 20 Slovenian */ + "sr" , TEXTS_sr, /* 11/ 93 Serbian */ + "sv" , TEXTS_sv, /* 21/ 54 Swedish */ + "sw" , TEXTS_sw, /* 9/ 18 Swahili */ + "th" , TEXTS_th, /* 2/ 15 Thai */ + "tl" , TEXTS_tl, /* 7/ 9 Tagalog */ + "tr" , TEXTS_tr, /* 7/ 18 Turkish */ + "uk" , TEXTS_uk, /* 11/ 87 Ukrainian */ + "vi" , TEXTS_vi, /* 8/ 15 Vietnamese */ + "zu" , TEXTS_zu, /* 8/ 11 Zulu */ + "zz" , TEXTS_zz, /* 19/112 Alphabet */ + }; + + static { + for (int index = 0; index < NAMES.length; index++) { + sNameToIndexesMap.put(NAMES[index], index); + } + + for (int i = 0; i < LOCALES_AND_TEXTS.length; i += 2) { + final String locale = (String)LOCALES_AND_TEXTS[i]; + final String[] textsTable = (String[])LOCALES_AND_TEXTS[i + 1]; + sLocaleToTextsTableMap.put(locale, textsTable); + sTextsTableToLocaleMap.put(textsTable, locale); + } + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeysCache.java b/java/src/com/android/inputmethod/keyboard/internal/KeysCache.java index 7c2e3e174..7743d4744 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeysCache.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeysCache.java @@ -17,12 +17,11 @@ package com.android.inputmethod.keyboard.internal; import com.android.inputmethod.keyboard.Key; -import com.android.inputmethod.latin.utils.CollectionUtils; import java.util.HashMap; public final class KeysCache { - private final HashMap<Key, Key> mMap = CollectionUtils.newHashMap(); + private final HashMap<Key, Key> mMap = new HashMap<>(); public void clear() { mMap.clear(); diff --git a/java/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelper.java b/java/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelper.java new file mode 100644 index 000000000..6400a2440 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelper.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.internal; + +import android.view.inputmethod.InputMethodSubtype; + +import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; + +import java.util.Collections; +import java.util.List; + +/** + * This class determines that the language name on the spacebar should be displayed in what format. + */ +public final class LanguageOnSpacebarHelper { + public static final int FORMAT_TYPE_NONE = 0; + public static final int FORMAT_TYPE_LANGUAGE_ONLY = 1; + public static final int FORMAT_TYPE_FULL_LOCALE = 2; + + private List<InputMethodSubtype> mEnabledSubtypes = Collections.emptyList(); + private boolean mIsSystemLanguageSameAsInputLanguage; + + public int getLanguageOnSpacebarFormatType(final InputMethodSubtype subtype) { + if (SubtypeLocaleUtils.isNoLanguage(subtype)) { + return FORMAT_TYPE_FULL_LOCALE; + } + // Only this subtype is enabled and equals to the system locale. + if (mEnabledSubtypes.size() < 2 && mIsSystemLanguageSameAsInputLanguage) { + return FORMAT_TYPE_NONE; + } + final String keyboardLanguage = SubtypeLocaleUtils.getSubtypeLocale(subtype).getLanguage(); + final String keyboardLayout = SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype); + int sameLanguageAndLayoutCount = 0; + for (final InputMethodSubtype ims : mEnabledSubtypes) { + final String language = SubtypeLocaleUtils.getSubtypeLocale(ims).getLanguage(); + if (keyboardLanguage.equals(language) && keyboardLayout.equals( + SubtypeLocaleUtils.getKeyboardLayoutSetName(ims))) { + sameLanguageAndLayoutCount++; + } + } + // Display full locale name only when there are multiple subtypes that have the same + // locale and keyboard layout. Otherwise displaying language name is enough. + return sameLanguageAndLayoutCount > 1 ? FORMAT_TYPE_FULL_LOCALE + : FORMAT_TYPE_LANGUAGE_ONLY; + } + + public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes) { + mEnabledSubtypes = enabledSubtypes; + } + + public void updateIsSystemLanguageSameAsInputLanguage(final boolean isSame) { + mIsSystemLanguageSameAsInputLanguage = isSame; + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java index 110936f8f..e0d5173ac 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java +++ b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java @@ -18,23 +18,42 @@ package com.android.inputmethod.keyboard.internal; import android.text.TextUtils; +import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.LatinImeLogger; +import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.StringUtils; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Locale; +/** + * The more key specification object. The more keys are an array of {@link MoreKeySpec}. + * + * The more keys specification is comma separated "key specification" each of which represents one + * "more key". + * The key specification might have label or string resource reference in it. These references are + * expanded before parsing comma. + * Special character, comma ',' backslash '\' can be escaped by '\' character. + * Note that the '\' is also parsed by XML parser and {@link MoreKeySpec#splitKeySpecs(String)} + * as well. + */ +// TODO: Should extend the key specification object. public final class MoreKeySpec { public final int mCode; public final String mLabel; public final String mOutputText; public final int mIconId; - public MoreKeySpec(final String moreKeySpec, boolean needsToUpperCase, final Locale locale, - final KeyboardCodesSet codesSet) { - mLabel = KeySpecParser.toUpperCaseOfStringForLocale( + public MoreKeySpec(final String moreKeySpec, boolean needsToUpperCase, final Locale locale) { + if (TextUtils.isEmpty(moreKeySpec)) { + throw new KeySpecParser.KeySpecParserError("Empty more key spec"); + } + mLabel = StringUtils.toUpperCaseOfStringForLocale( KeySpecParser.getLabel(moreKeySpec), needsToUpperCase, locale); - final int code = KeySpecParser.toUpperCaseOfCodeForLocale( - KeySpecParser.getCode(moreKeySpec, codesSet), needsToUpperCase, locale); + final int code = StringUtils.toUpperCaseOfCodeForLocale( + KeySpecParser.getCode(moreKeySpec), needsToUpperCase, locale); if (code == Constants.CODE_UNSPECIFIED) { // Some letter, for example German Eszett (U+00DF: "ß"), has multiple characters // upper case representation ("SS"). @@ -42,12 +61,19 @@ public final class MoreKeySpec { mOutputText = mLabel; } else { mCode = code; - mOutputText = KeySpecParser.toUpperCaseOfStringForLocale( + mOutputText = StringUtils.toUpperCaseOfStringForLocale( KeySpecParser.getOutputText(moreKeySpec), needsToUpperCase, locale); } mIconId = KeySpecParser.getIconId(moreKeySpec); } + public Key buildKey(final int x, final int y, final int labelFlags, + final KeyboardParams params) { + return new Key(mLabel, mIconId, mCode, mOutputText, null /* hintLabel */, labelFlags, + Key.BACKGROUND_TYPE_NORMAL, x, y, params.mDefaultKeyWidth, params.mDefaultRowHeight, + params.mHorizontalGap, params.mVerticalGap); + } + @Override public int hashCode() { int hashCode = 1; @@ -74,7 +100,7 @@ public final class MoreKeySpec { @Override public String toString() { final String label = (mIconId == KeyboardIconsSet.ICON_UNDEFINED ? mLabel - : KeySpecParser.PREFIX_ICON + KeyboardIconsSet.getIconName(mIconId)); + : KeyboardIconsSet.PREFIX_ICON + KeyboardIconsSet.getIconName(mIconId)); final String output = (mCode == Constants.CODE_OUTPUT_TEXT ? mOutputText : Constants.printableCode(mCode)); if (StringUtils.codePointCount(label) == 1 && label.codePointAt(0) == mCode) { @@ -83,4 +109,196 @@ public final class MoreKeySpec { return label + "|" + output; } } + + private static final boolean DEBUG = LatinImeLogger.sDBG; + // Constants for parsing. + private static final char COMMA = Constants.CODE_COMMA; + private static final char BACKSLASH = Constants.CODE_BACKSLASH; + private static final String ADDITIONAL_MORE_KEY_MARKER = + StringUtils.newSingleCodePointString(Constants.CODE_PERCENT); + + /** + * Split the text containing multiple key specifications separated by commas into an array of + * key specifications. + * A key specification can contain a character escaped by the backslash character, including a + * comma character. + * Note that an empty key specification will be eliminated from the result array. + * + * @param text the text containing multiple key specifications. + * @return an array of key specification text. Null if the specified <code>text</code> is empty + * or has no key specifications. + */ + public static String[] splitKeySpecs(final String text) { + if (TextUtils.isEmpty(text)) { + return null; + } + final int size = text.length(); + // Optimization for one-letter key specification. + if (size == 1) { + return text.charAt(0) == COMMA ? null : new String[] { text }; + } + + ArrayList<String> list = null; + int start = 0; + // The characters in question in this loop are COMMA and BACKSLASH. These characters never + // match any high or low surrogate character. So it is OK to iterate through with char + // index. + for (int pos = 0; pos < size; pos++) { + final char c = text.charAt(pos); + if (c == COMMA) { + // Skip empty entry. + if (pos - start > 0) { + if (list == null) { + list = new ArrayList<>(); + } + list.add(text.substring(start, pos)); + } + // Skip comma + start = pos + 1; + } else if (c == BACKSLASH) { + // Skip escape character and escaped character. + pos++; + } + } + final String remain = (size - start > 0) ? text.substring(start) : null; + if (list == null) { + return remain != null ? new String[] { remain } : null; + } + if (remain != null) { + list.add(remain); + } + return list.toArray(new String[list.size()]); + } + + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + + private static String[] filterOutEmptyString(final String[] array) { + if (array == null) { + return EMPTY_STRING_ARRAY; + } + ArrayList<String> out = null; + for (int i = 0; i < array.length; i++) { + final String entry = array[i]; + if (TextUtils.isEmpty(entry)) { + if (out == null) { + out = CollectionUtils.arrayAsList(array, 0, i); + } + } else if (out != null) { + out.add(entry); + } + } + if (out == null) { + return array; + } + return out.toArray(new String[out.size()]); + } + + public static String[] insertAdditionalMoreKeys(final String[] moreKeySpecs, + final String[] additionalMoreKeySpecs) { + final String[] moreKeys = filterOutEmptyString(moreKeySpecs); + final String[] additionalMoreKeys = filterOutEmptyString(additionalMoreKeySpecs); + final int moreKeysCount = moreKeys.length; + final int additionalCount = additionalMoreKeys.length; + ArrayList<String> out = null; + int additionalIndex = 0; + for (int moreKeyIndex = 0; moreKeyIndex < moreKeysCount; moreKeyIndex++) { + final String moreKeySpec = moreKeys[moreKeyIndex]; + if (moreKeySpec.equals(ADDITIONAL_MORE_KEY_MARKER)) { + if (additionalIndex < additionalCount) { + // Replace '%' marker with additional more key specification. + final String additionalMoreKey = additionalMoreKeys[additionalIndex]; + if (out != null) { + out.add(additionalMoreKey); + } else { + moreKeys[moreKeyIndex] = additionalMoreKey; + } + additionalIndex++; + } else { + // Filter out excessive '%' marker. + if (out == null) { + out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeyIndex); + } + } + } else { + if (out != null) { + out.add(moreKeySpec); + } + } + } + if (additionalCount > 0 && additionalIndex == 0) { + // No '%' marker is found in more keys. + // Insert all additional more keys to the head of more keys. + if (DEBUG && out != null) { + throw new RuntimeException("Internal logic error:" + + " moreKeys=" + Arrays.toString(moreKeys) + + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys)); + } + out = CollectionUtils.arrayAsList(additionalMoreKeys, additionalIndex, additionalCount); + for (int i = 0; i < moreKeysCount; i++) { + out.add(moreKeys[i]); + } + } else if (additionalIndex < additionalCount) { + // The number of '%' markers are less than additional more keys. + // Append remained additional more keys to the tail of more keys. + if (DEBUG && out != null) { + throw new RuntimeException("Internal logic error:" + + " moreKeys=" + Arrays.toString(moreKeys) + + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys)); + } + out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeysCount); + for (int i = additionalIndex; i < additionalCount; i++) { + out.add(additionalMoreKeys[additionalIndex]); + } + } + if (out == null && moreKeysCount > 0) { + return moreKeys; + } else if (out != null && out.size() > 0) { + return out.toArray(new String[out.size()]); + } else { + return null; + } + } + + public static int getIntValue(final String[] moreKeys, final String key, + final int defaultValue) { + if (moreKeys == null) { + return defaultValue; + } + final int keyLen = key.length(); + boolean foundValue = false; + int value = defaultValue; + for (int i = 0; i < moreKeys.length; i++) { + final String moreKeySpec = moreKeys[i]; + if (moreKeySpec == null || !moreKeySpec.startsWith(key)) { + continue; + } + moreKeys[i] = null; + try { + if (!foundValue) { + value = Integer.parseInt(moreKeySpec.substring(keyLen)); + foundValue = true; + } + } catch (NumberFormatException e) { + throw new RuntimeException( + "integer should follow after " + key + ": " + moreKeySpec); + } + } + return value; + } + + public static boolean getBooleanValue(final String[] moreKeys, final String key) { + if (moreKeys == null) { + return false; + } + boolean value = false; + for (int i = 0; i < moreKeys.length; i++) { + final String moreKeySpec = moreKeys[i]; + if (moreKeySpec == null || !moreKeySpec.equals(key)) { + continue; + } + moreKeys[i] = null; + value = true; + } + return value; + } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/NonDistinctMultitouchHelper.java b/java/src/com/android/inputmethod/keyboard/internal/NonDistinctMultitouchHelper.java index a0935b985..3a9aa81a3 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/NonDistinctMultitouchHelper.java +++ b/java/src/com/android/inputmethod/keyboard/internal/NonDistinctMultitouchHelper.java @@ -20,18 +20,19 @@ import android.util.Log; import android.view.MotionEvent; import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.keyboard.KeyDetector; import com.android.inputmethod.keyboard.PointerTracker; -import com.android.inputmethod.keyboard.PointerTracker.KeyEventHandler; import com.android.inputmethod.latin.utils.CoordinateUtils; public final class NonDistinctMultitouchHelper { private static final String TAG = NonDistinctMultitouchHelper.class.getSimpleName(); + private static final int MAIN_POINTER_TRACKER_ID = 0; private int mOldPointerCount = 1; private Key mOldKey; private int[] mLastCoords = CoordinateUtils.newInstance(); - public void processMotionEvent(final MotionEvent me, final KeyEventHandler keyEventHandler) { + public void processMotionEvent(final MotionEvent me, final KeyDetector keyDetector) { final int pointerCount = me.getPointerCount(); final int oldPointerCount = mOldPointerCount; mOldPointerCount = pointerCount; @@ -41,8 +42,9 @@ public final class NonDistinctMultitouchHelper { return; } - // Use only main (id=0) pointer tracker. - final PointerTracker mainTracker = PointerTracker.getPointerTracker(0, keyEventHandler); + // Use only main pointer tracker. + final PointerTracker mainTracker = PointerTracker.getPointerTracker( + MAIN_POINTER_TRACKER_ID); final int action = me.getActionMasked(); final int index = me.getActionIndex(); final long eventTime = me.getEventTime(); @@ -51,12 +53,12 @@ public final class NonDistinctMultitouchHelper { // In single-touch. if (oldPointerCount == 1 && pointerCount == 1) { if (me.getPointerId(index) == mainTracker.mPointerId) { - mainTracker.processMotionEvent(me, keyEventHandler); + mainTracker.processMotionEvent(me, keyDetector); return; } // Inject a copied event. injectMotionEvent(action, me.getX(index), me.getY(index), downTime, eventTime, - mainTracker, keyEventHandler); + mainTracker, keyDetector); return; } @@ -70,7 +72,7 @@ public final class NonDistinctMultitouchHelper { mOldKey = mainTracker.getKeyOn(x, y); // Inject an artifact up event for the old key. injectMotionEvent(MotionEvent.ACTION_UP, x, y, downTime, eventTime, - mainTracker, keyEventHandler); + mainTracker, keyDetector); return; } @@ -85,11 +87,11 @@ public final class NonDistinctMultitouchHelper { // Inject an artifact down event for the new key. // An artifact up event for the new key will usually be injected as a single-touch. injectMotionEvent(MotionEvent.ACTION_DOWN, x, y, downTime, eventTime, - mainTracker, keyEventHandler); + mainTracker, keyDetector); if (action == MotionEvent.ACTION_UP) { // Inject an artifact up event for the new key also. injectMotionEvent(MotionEvent.ACTION_UP, x, y, downTime, eventTime, - mainTracker, keyEventHandler); + mainTracker, keyDetector); } } return; @@ -101,11 +103,11 @@ public final class NonDistinctMultitouchHelper { private static void injectMotionEvent(final int action, final float x, final float y, final long downTime, final long eventTime, final PointerTracker tracker, - final KeyEventHandler handler) { + final KeyDetector keyDetector) { final MotionEvent me = MotionEvent.obtain( downTime, eventTime, action, x, y, 0 /* metaState */); try { - tracker.processMotionEvent(me, handler); + tracker.processMotionEvent(me, keyDetector); } finally { me.recycle(); } diff --git a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java index 7ee45e8f6..8e89e61ea 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java +++ b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java @@ -18,8 +18,6 @@ package com.android.inputmethod.keyboard.internal; import android.util.Log; -import com.android.inputmethod.latin.utils.CollectionUtils; - import java.util.ArrayList; public final class PointerTrackerQueue { @@ -28,7 +26,7 @@ public final class PointerTrackerQueue { public interface Element { public boolean isModifier(); - public boolean isInSlidingKeyInput(); + public boolean isInDraggingFinger(); public void onPhantomUpEvent(long eventTime); public void cancelTrackingForAction(); } @@ -37,7 +35,7 @@ public final class PointerTrackerQueue { // Note: {@link #mExpandableArrayOfActivePointers} and {@link #mArraySize} are synchronized by // {@link #mExpandableArrayOfActivePointers} private final ArrayList<Element> mExpandableArrayOfActivePointers = - CollectionUtils.newArrayList(INITIAL_CAPACITY); + new ArrayList<>(INITIAL_CAPACITY); private int mArraySize = 0; public int size() { @@ -193,13 +191,13 @@ public final class PointerTrackerQueue { } } - public boolean isAnyInSlidingKeyInput() { + public boolean isAnyInDraggingFinger() { synchronized (mExpandableArrayOfActivePointers) { final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; final int arraySize = mArraySize; for (int index = 0; index < arraySize; index++) { final Element element = expandableArray.get(index); - if (element.isInSlidingKeyInput()) { + if (element.isInDraggingFinger()) { return true; } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/ScrollKeyboardView.java b/java/src/com/android/inputmethod/keyboard/internal/ScrollKeyboardView.java deleted file mode 100644 index 9cf68d43d..000000000 --- a/java/src/com/android/inputmethod/keyboard/internal/ScrollKeyboardView.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.keyboard.internal; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.GestureDetector; -import android.view.MotionEvent; -import android.widget.ScrollView; -import android.widget.Scroller; - -import com.android.inputmethod.keyboard.Key; -import com.android.inputmethod.keyboard.KeyDetector; -import com.android.inputmethod.keyboard.Keyboard; -import com.android.inputmethod.keyboard.KeyboardView; -import com.android.inputmethod.latin.R; - -/** - * This is an extended {@link KeyboardView} class that hosts a vertical scroll keyboard. - * Multi-touch unsupported. No {@link PointerTracker}s. No gesture support. - * TODO: Vertical scroll capability should be removed from this class because it's no longer used. - */ -// TODO: Implement key popup preview. -public final class ScrollKeyboardView extends KeyboardView implements - ScrollViewWithNotifier.ScrollListener, GestureDetector.OnGestureListener { - private static final boolean PAGINATION = false; - - public interface OnKeyClickListener { - public void onKeyClick(Key key); - } - - private static final OnKeyClickListener EMPTY_LISTENER = new OnKeyClickListener() { - @Override - public void onKeyClick(final Key key) {} - }; - - private OnKeyClickListener mListener = EMPTY_LISTENER; - private final KeyDetector mKeyDetector = new KeyDetector(0.0f /*keyHysteresisDistance */); - private final GestureDetector mGestureDetector; - - private final Scroller mScroller; - private ScrollViewWithNotifier mScrollView; - - public ScrollKeyboardView(final Context context, final AttributeSet attrs) { - this(context, attrs, R.attr.keyboardViewStyle); - } - - public ScrollKeyboardView(final Context context, final AttributeSet attrs, final int defStyle) { - super(context, attrs, defStyle); - mGestureDetector = new GestureDetector(context, this); - mGestureDetector.setIsLongpressEnabled(false /* isLongpressEnabled */); - mScroller = new Scroller(context); - } - - public void setScrollView(final ScrollViewWithNotifier scrollView) { - mScrollView = scrollView; - scrollView.setScrollListener(this); - } - - private final Runnable mScrollTask = new Runnable() { - @Override - public void run() { - final Scroller scroller = mScroller; - final ScrollView scrollView = mScrollView; - scroller.computeScrollOffset(); - scrollView.scrollTo(0, scroller.getCurrY()); - if (!scroller.isFinished()) { - scrollView.post(this); - } - } - }; - - // {@link ScrollViewWithNotified#ScrollListener} methods. - @Override - public void notifyScrollChanged(final int scrollX, final int scrollY, final int oldX, - final int oldY) { - if (PAGINATION) { - mScroller.forceFinished(true /* finished */); - mScrollView.removeCallbacks(mScrollTask); - final int currentTop = mScrollView.getScrollY(); - final int pageHeight = getKeyboard().mBaseHeight; - final int lastPageNo = currentTop / pageHeight; - final int lastPageTop = lastPageNo * pageHeight; - final int nextPageNo = lastPageNo + 1; - final int nextPageTop = Math.min(nextPageNo * pageHeight, getHeight() - pageHeight); - final int scrollTo = (currentTop - lastPageTop) < (nextPageTop - currentTop) - ? lastPageTop : nextPageTop; - final int deltaY = scrollTo - currentTop; - mScroller.startScroll(0, currentTop, 0, deltaY, 300); - mScrollView.post(mScrollTask); - } - } - - @Override - public void notifyOverScrolled(final int scrollX, final int scrollY, final boolean clampedX, - final boolean clampedY) { - releaseCurrentKey(); - } - - public void setOnKeyClickListener(final OnKeyClickListener listener) { - mListener = listener; - } - - /** - * {@inheritDoc} - */ - @Override - public void setKeyboard(final Keyboard keyboard) { - super.setKeyboard(keyboard); - mKeyDetector.setKeyboard(keyboard, 0 /* correctionX */, 0 /* correctionY */); - } - - /** - * {@inheritDoc} - */ - @Override - public boolean onTouchEvent(final MotionEvent e) { - if (mGestureDetector.onTouchEvent(e)) { - return true; - } - final Key key = getKey(e); - if (key != null && key != mCurrentKey) { - releaseCurrentKey(); - } - return true; - } - - // {@link GestureDetector#OnGestureListener} methods. - private Key mCurrentKey; - - private Key getKey(final MotionEvent e) { - final int index = e.getActionIndex(); - final int x = (int)e.getX(index); - final int y = (int)e.getY(index); - return mKeyDetector.detectHitKey(x, y); - } - - public void releaseCurrentKey() { - final Key currentKey = mCurrentKey; - if (currentKey == null) { - return; - } - currentKey.onReleased(); - invalidateKey(currentKey); - mCurrentKey = null; - } - - @Override - public boolean onDown(final MotionEvent e) { - final Key key = getKey(e); - releaseCurrentKey(); - mCurrentKey = key; - if (key == null) { - return false; - } - // TODO: May call {@link KeyboardActionListener#onPressKey(int,int,boolean)}. - key.onPressed(); - invalidateKey(key); - return false; - } - - @Override - public void onShowPress(final MotionEvent e) { - // User feedback is done at {@link #onDown(MotionEvent)}. - } - - @Override - public boolean onSingleTapUp(final MotionEvent e) { - final Key key = getKey(e); - releaseCurrentKey(); - if (key == null) { - return false; - } - // TODO: May call {@link KeyboardActionListener#onReleaseKey(int,boolean)}. - key.onReleased(); - invalidateKey(key); - mListener.onKeyClick(key); - return true; - } - - @Override - public boolean onScroll(final MotionEvent e1, final MotionEvent e2, final float distanceX, - final float distanceY) { - releaseCurrentKey(); - return false; - } - - @Override - public boolean onFling(final MotionEvent e1, final MotionEvent e2, final float velocityX, - final float velocityY) { - releaseCurrentKey(); - return false; - } - - @Override - public void onLongPress(final MotionEvent e) { - // Long press detection of {@link #mGestureDetector} is disabled and not used. - } -} diff --git a/java/src/com/android/inputmethod/keyboard/internal/ScrollViewWithNotifier.java b/java/src/com/android/inputmethod/keyboard/internal/ScrollViewWithNotifier.java deleted file mode 100644 index d1ccdc7b5..000000000 --- a/java/src/com/android/inputmethod/keyboard/internal/ScrollViewWithNotifier.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.keyboard.internal; - -import android.content.Context; -import android.util.AttributeSet; -import android.widget.ScrollView; - -/** - * This is an extended {@link ScrollView} that can notify - * {@link ScrollView#onScrollChanged(int,int,int,int} and - * {@link ScrollView#onOverScrolled(int,int,int,int)} to a content view. - */ -public class ScrollViewWithNotifier extends ScrollView { - private ScrollListener mScrollListener = EMPTY_LISTER; - - public interface ScrollListener { - public void notifyScrollChanged(int scrollX, int scrollY, int oldX, int oldY); - public void notifyOverScrolled(int scrollX, int scrollY, boolean clampedX, - boolean clampedY); - } - - private static final ScrollListener EMPTY_LISTER = new ScrollListener() { - @Override - public void notifyScrollChanged(int scrollX, int scrollY, int oldX, int oldY) {} - @Override - public void notifyOverScrolled(int scrollX, int scrollY, boolean clampedX, - boolean clampedY) {} - }; - - public ScrollViewWithNotifier(final Context context, final AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onScrollChanged(final int scrollX, final int scrollY, final int oldX, - final int oldY) { - super.onScrollChanged(scrollX, scrollY, oldX, oldY); - mScrollListener.notifyScrollChanged(scrollX, scrollY, oldX, oldY); - } - - @Override - protected void onOverScrolled(final int scrollX, final int scrollY, final boolean clampedX, - final boolean clampedY) { - super.onOverScrolled(scrollX, scrollY, clampedX, clampedY); - mScrollListener.notifyOverScrolled(scrollX, scrollY, clampedX, clampedY); - } - - public void setScrollListener(final ScrollListener listener) { - mScrollListener = listener; - } -} diff --git a/java/src/com/android/inputmethod/keyboard/internal/SlidingKeyInputPreview.java b/java/src/com/android/inputmethod/keyboard/internal/SlidingKeyInputDrawingPreview.java index 2787ebfb9..ef4c74d61 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/SlidingKeyInputPreview.java +++ b/java/src/com/android/inputmethod/keyboard/internal/SlidingKeyInputDrawingPreview.java @@ -20,7 +20,6 @@ import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; -import android.view.View; import com.android.inputmethod.keyboard.PointerTracker; import com.android.inputmethod.latin.R; @@ -28,8 +27,13 @@ import com.android.inputmethod.latin.utils.CoordinateUtils; /** * Draw rubber band preview graphics during sliding key input. + * + * @attr ref R.styleable#MainKeyboardView_slidingKeyInputPreviewColor + * @attr ref R.styleable#MainKeyboardView_slidingKeyInputPreviewWidth + * @attr ref R.styleable#MainKeyboardView_slidingKeyInputPreviewBodyRatio + * @attr ref R.styleable#MainKeyboardView_slidingKeyInputPreviewShadowRatio */ -public final class SlidingKeyInputPreview extends AbstractDrawingPreview { +public final class SlidingKeyInputDrawingPreview extends AbstractDrawingPreview { private final float mPreviewBodyRadius; private boolean mShowsSlidingKeyInputPreview; @@ -40,8 +44,7 @@ public final class SlidingKeyInputPreview extends AbstractDrawingPreview { private final RoundedLine mRoundedLine = new RoundedLine(); private final Paint mPaint = new Paint(); - public SlidingKeyInputPreview(final View drawingView, final TypedArray mainKeyboardViewAttr) { - super(drawingView); + public SlidingKeyInputDrawingPreview(final TypedArray mainKeyboardViewAttr) { final int previewColor = mainKeyboardViewAttr.getColor( R.styleable.MainKeyboardView_slidingKeyInputPreviewColor, 0); final float previewRadius = mainKeyboardViewAttr.getDimension( @@ -61,9 +64,14 @@ public final class SlidingKeyInputPreview extends AbstractDrawingPreview { mPaint.setColor(previewColor); } + @Override + public void onDeallocateMemory() { + // Nothing to do here. + } + public void dismissSlidingKeyInputPreview() { mShowsSlidingKeyInputPreview = false; - getDrawingView().invalidate(); + invalidateDrawingView(); } /** @@ -93,6 +101,6 @@ public final class SlidingKeyInputPreview extends AbstractDrawingPreview { tracker.getDownCoordinates(mPreviewFrom); tracker.getLastCoordinates(mPreviewTo); mShowsSlidingKeyInputPreview = true; - getDrawingView().invalidate(); + invalidateDrawingView(); } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/TimerHandler.java b/java/src/com/android/inputmethod/keyboard/internal/TimerHandler.java new file mode 100644 index 000000000..ec7b9b024 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/TimerHandler.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.internal; + +import android.os.Message; +import android.os.SystemClock; +import android.view.ViewConfiguration; + +import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.keyboard.PointerTracker; +import com.android.inputmethod.keyboard.PointerTracker.TimerProxy; +import com.android.inputmethod.keyboard.internal.TimerHandler.Callbacks; +import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper; + +// TODO: Separate this class into KeyTimerHandler and BatchInputTimerHandler or so. +public final class TimerHandler extends LeakGuardHandlerWrapper<Callbacks> implements TimerProxy { + public interface Callbacks { + public void startWhileTypingFadeinAnimation(); + public void startWhileTypingFadeoutAnimation(); + public void onLongPress(PointerTracker tracker); + } + + private static final int MSG_TYPING_STATE_EXPIRED = 0; + private static final int MSG_REPEAT_KEY = 1; + private static final int MSG_LONGPRESS_KEY = 2; + private static final int MSG_LONGPRESS_SHIFT_KEY = 3; + private static final int MSG_DOUBLE_TAP_SHIFT_KEY = 4; + private static final int MSG_UPDATE_BATCH_INPUT = 5; + + private final int mIgnoreAltCodeKeyTimeout; + private final int mGestureRecognitionUpdateTime; + + public TimerHandler(final Callbacks ownerInstance, final int ignoreAltCodeKeyTimeout, + final int gestureRecognitionUpdateTime) { + super(ownerInstance); + mIgnoreAltCodeKeyTimeout = ignoreAltCodeKeyTimeout; + mGestureRecognitionUpdateTime = gestureRecognitionUpdateTime; + } + + @Override + public void handleMessage(final Message msg) { + final Callbacks callbacks = getOwnerInstance(); + if (callbacks == null) { + return; + } + final PointerTracker tracker = (PointerTracker) msg.obj; + switch (msg.what) { + case MSG_TYPING_STATE_EXPIRED: + callbacks.startWhileTypingFadeinAnimation(); + break; + case MSG_REPEAT_KEY: + tracker.onKeyRepeat(msg.arg1 /* code */, msg.arg2 /* repeatCount */); + break; + case MSG_LONGPRESS_KEY: + case MSG_LONGPRESS_SHIFT_KEY: + cancelLongPressTimers(); + callbacks.onLongPress(tracker); + break; + case MSG_UPDATE_BATCH_INPUT: + tracker.updateBatchInputByTimer(SystemClock.uptimeMillis()); + startUpdateBatchInputTimer(tracker); + break; + } + } + + @Override + public void startKeyRepeatTimerOf(final PointerTracker tracker, final int repeatCount, + final int delay) { + final Key key = tracker.getKey(); + if (key == null || delay == 0) { + return; + } + sendMessageDelayed( + obtainMessage(MSG_REPEAT_KEY, key.getCode(), repeatCount, tracker), delay); + } + + private void cancelKeyRepeatTimerOf(final PointerTracker tracker) { + removeMessages(MSG_REPEAT_KEY, tracker); + } + + public void cancelKeyRepeatTimers() { + removeMessages(MSG_REPEAT_KEY); + } + + // TODO: Suppress layout changes in key repeat mode + public boolean isInKeyRepeat() { + return hasMessages(MSG_REPEAT_KEY); + } + + @Override + public void startLongPressTimerOf(final PointerTracker tracker, final int delay) { + final Key key = tracker.getKey(); + if (key == null) { + return; + } + // Use a separate message id for long pressing shift key, because long press shift key + // timers should be canceled when other key is pressed. + final int messageId = (key.getCode() == Constants.CODE_SHIFT) + ? MSG_LONGPRESS_SHIFT_KEY : MSG_LONGPRESS_KEY; + sendMessageDelayed(obtainMessage(messageId, tracker), delay); + } + + @Override + public void cancelLongPressTimerOf(final PointerTracker tracker) { + removeMessages(MSG_LONGPRESS_KEY, tracker); + removeMessages(MSG_LONGPRESS_SHIFT_KEY, tracker); + } + + @Override + public void cancelLongPressShiftKeyTimers() { + removeMessages(MSG_LONGPRESS_SHIFT_KEY); + } + + public void cancelLongPressTimers() { + removeMessages(MSG_LONGPRESS_KEY); + removeMessages(MSG_LONGPRESS_SHIFT_KEY); + } + + @Override + public void startTypingStateTimer(final Key typedKey) { + if (typedKey.isModifier() || typedKey.altCodeWhileTyping()) { + return; + } + + final boolean isTyping = isTypingState(); + removeMessages(MSG_TYPING_STATE_EXPIRED); + final Callbacks callbacks = getOwnerInstance(); + if (callbacks == null) { + return; + } + + // When user hits the space or the enter key, just cancel the while-typing timer. + final int typedCode = typedKey.getCode(); + if (typedCode == Constants.CODE_SPACE || typedCode == Constants.CODE_ENTER) { + if (isTyping) { + callbacks.startWhileTypingFadeinAnimation(); + } + return; + } + + sendMessageDelayed( + obtainMessage(MSG_TYPING_STATE_EXPIRED), mIgnoreAltCodeKeyTimeout); + if (isTyping) { + return; + } + callbacks.startWhileTypingFadeoutAnimation(); + } + + @Override + public boolean isTypingState() { + return hasMessages(MSG_TYPING_STATE_EXPIRED); + } + + @Override + public void startDoubleTapShiftKeyTimer() { + sendMessageDelayed(obtainMessage(MSG_DOUBLE_TAP_SHIFT_KEY), + ViewConfiguration.getDoubleTapTimeout()); + } + + @Override + public void cancelDoubleTapShiftKeyTimer() { + removeMessages(MSG_DOUBLE_TAP_SHIFT_KEY); + } + + @Override + public boolean isInDoubleTapShiftKeyTimeout() { + return hasMessages(MSG_DOUBLE_TAP_SHIFT_KEY); + } + + @Override + public void cancelKeyTimersOf(final PointerTracker tracker) { + cancelKeyRepeatTimerOf(tracker); + cancelLongPressTimerOf(tracker); + } + + public void cancelAllKeyTimers() { + cancelKeyRepeatTimers(); + cancelLongPressTimers(); + } + + @Override + public void startUpdateBatchInputTimer(final PointerTracker tracker) { + if (mGestureRecognitionUpdateTime <= 0) { + return; + } + removeMessages(MSG_UPDATE_BATCH_INPUT, tracker); + sendMessageDelayed(obtainMessage(MSG_UPDATE_BATCH_INPUT, tracker), + mGestureRecognitionUpdateTime); + } + + @Override + public void cancelUpdateBatchInputTimer(final PointerTracker tracker) { + removeMessages(MSG_UPDATE_BATCH_INPUT, tracker); + } + + @Override + public void cancelAllUpdateBatchInputTimers() { + removeMessages(MSG_UPDATE_BATCH_INPUT); + } + + public void cancelAllMessages() { + cancelAllKeyTimers(); + cancelAllUpdateBatchInputTimers(); + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/TypingTimeRecorder.java b/java/src/com/android/inputmethod/keyboard/internal/TypingTimeRecorder.java new file mode 100644 index 000000000..9593f715c --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/TypingTimeRecorder.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.internal; + +public final class TypingTimeRecorder { + private final int mStaticTimeThresholdAfterFastTyping; // msec + private final int mSuppressKeyPreviewAfterBatchInputDuration; + private long mLastTypingTime; + private long mLastLetterTypingTime; + private long mLastBatchInputTime; + + public TypingTimeRecorder(final int staticTimeThresholdAfterFastTyping, + final int suppressKeyPreviewAfterBatchInputDuration) { + mStaticTimeThresholdAfterFastTyping = staticTimeThresholdAfterFastTyping; + mSuppressKeyPreviewAfterBatchInputDuration = suppressKeyPreviewAfterBatchInputDuration; + } + + public boolean isInFastTyping(final long eventTime) { + final long elapsedTimeSinceLastLetterTyping = eventTime - mLastLetterTypingTime; + return elapsedTimeSinceLastLetterTyping < mStaticTimeThresholdAfterFastTyping; + } + + private boolean wasLastInputTyping() { + return mLastTypingTime >= mLastBatchInputTime; + } + + public void onCodeInput(final int code, final long eventTime) { + // Record the letter typing time when + // 1. Letter keys are typed successively without any batch input in between. + // 2. A letter key is typed within the threshold time since the last any key typing. + // 3. A non-letter key is typed within the threshold time since the last letter key typing. + if (Character.isLetter(code)) { + if (wasLastInputTyping() + || eventTime - mLastTypingTime < mStaticTimeThresholdAfterFastTyping) { + mLastLetterTypingTime = eventTime; + } + } else { + if (eventTime - mLastLetterTypingTime < mStaticTimeThresholdAfterFastTyping) { + // This non-letter typing should be treated as a part of fast typing. + mLastLetterTypingTime = eventTime; + } + } + mLastTypingTime = eventTime; + } + + public void onEndBatchInput(final long eventTime) { + mLastBatchInputTime = eventTime; + } + + public long getLastLetterTypingTime() { + return mLastLetterTypingTime; + } + + public boolean needsToSuppressKeyPreviewPopup(final long eventTime) { + return !wasLastInputTyping() + && eventTime - mLastBatchInputTime < mSuppressKeyPreviewAfterBatchInputDuration; + } +} diff --git a/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java b/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java deleted file mode 100644 index 463d09344..000000000 --- a/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin; - -import android.content.Context; -import android.util.Log; - -import com.android.inputmethod.latin.makedict.DictEncoder; -import com.android.inputmethod.latin.makedict.UnsupportedFormatException; -import com.android.inputmethod.latin.makedict.Ver3DictEncoder; - -import java.io.File; -import java.io.IOException; -import java.util.Map; - -// TODO: Quit extending Dictionary after implementing dynamic binary dictionary. -abstract public class AbstractDictionaryWriter extends Dictionary { - /** Used for Log actions from this class */ - private static final String TAG = AbstractDictionaryWriter.class.getSimpleName(); - - private final Context mContext; - - public AbstractDictionaryWriter(final Context context, final String dictType) { - super(dictType); - mContext = context; - } - - abstract public void clear(); - - /** - * Add a unigram with an optional shortcut to the dictionary. - * @param word The word to add. - * @param shortcutTarget A shortcut target for this word, or null if none. - * @param frequency The frequency for this unigram. - * @param shortcutFreq The frequency of the shortcut (0~15, with 15 = whitelist). Ignored - * if shortcutTarget is null. - * @param isNotAWord true if this is not a word, i.e. shortcut only. - */ - abstract public void addUnigramWord(final String word, final String shortcutTarget, - final int frequency, final int shortcutFreq, final boolean isNotAWord); - - // TODO: Remove lastModifiedTime after making binary dictionary support forgetting curve. - abstract public void addBigramWords(final String word0, final String word1, - final int frequency, final boolean isValid, - final long lastModifiedTime); - - abstract public void removeBigramWords(final String word0, final String word1); - - abstract protected void writeDictionary(final DictEncoder dictEncoder, - final Map<String, String> attributeMap) throws IOException, UnsupportedFormatException; - - public void write(final String fileName, final Map<String, String> attributeMap) { - final String tempFileName = fileName + ".temp"; - final File file = new File(mContext.getFilesDir(), fileName); - final File tempFile = new File(mContext.getFilesDir(), tempFileName); - try { - final DictEncoder dictEncoder = new Ver3DictEncoder(tempFile); - writeDictionary(dictEncoder, attributeMap); - tempFile.renameTo(file); - } catch (IOException e) { - Log.e(TAG, "IO exception while writing file", e); - } catch (UnsupportedFormatException e) { - Log.e(TAG, "Unsupported format", e); - } - } -} diff --git a/java/src/com/android/inputmethod/latin/AssetFileAddress.java b/java/src/com/android/inputmethod/latin/AssetFileAddress.java index 875192554..fd6c24dfe 100644 --- a/java/src/com/android/inputmethod/latin/AssetFileAddress.java +++ b/java/src/com/android/inputmethod/latin/AssetFileAddress.java @@ -16,6 +16,8 @@ package com.android.inputmethod.latin; +import com.android.inputmethod.latin.utils.FileUtils; + import java.io.File; /** @@ -52,4 +54,12 @@ public final class AssetFileAddress { if (!f.isFile()) return null; return new AssetFileAddress(filename, offset, length); } + + public boolean pointsToPhysicalFile() { + return 0 == mOffset; + } + + public void deleteUnderlyingFile() { + FileUtils.deleteRecursively(new File(mFilename)); + } } diff --git a/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java b/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java index 54bc29559..eb8b34ccd 100644 --- a/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java +++ b/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java @@ -16,14 +16,14 @@ package com.android.inputmethod.latin; -import com.android.inputmethod.latin.settings.SettingsValues; - import android.content.Context; import android.media.AudioManager; import android.os.Vibrator; import android.view.HapticFeedbackConstants; import android.view.View; +import com.android.inputmethod.latin.settings.SettingsValues; + /** * This class gathers audio feedback and haptic feedback functions. * @@ -86,40 +86,41 @@ public final class AudioAndHapticFeedbackManager { if (mAudioManager == null) { return; } - if (mSoundOn) { - final int sound; - switch (code) { - case Constants.CODE_DELETE: - sound = AudioManager.FX_KEYPRESS_DELETE; - break; - case Constants.CODE_ENTER: - sound = AudioManager.FX_KEYPRESS_RETURN; - break; - case Constants.CODE_SPACE: - sound = AudioManager.FX_KEYPRESS_SPACEBAR; - break; - default: - sound = AudioManager.FX_KEYPRESS_STANDARD; - break; - } - mAudioManager.playSoundEffect(sound, mSettingsValues.mKeypressSoundVolume); + if (!mSoundOn) { + return; + } + final int sound; + switch (code) { + case Constants.CODE_DELETE: + sound = AudioManager.FX_KEYPRESS_DELETE; + break; + case Constants.CODE_ENTER: + sound = AudioManager.FX_KEYPRESS_RETURN; + break; + case Constants.CODE_SPACE: + sound = AudioManager.FX_KEYPRESS_SPACEBAR; + break; + default: + sound = AudioManager.FX_KEYPRESS_STANDARD; + break; } + mAudioManager.playSoundEffect(sound, mSettingsValues.mKeypressSoundVolume); } public void performHapticFeedback(final View viewToPerformHapticFeedbackOn) { if (!mSettingsValues.mVibrateOn) { return; } - if (mSettingsValues.mKeypressVibrationDuration < 0) { - // Go ahead with the system default - if (viewToPerformHapticFeedbackOn != null) { - viewToPerformHapticFeedbackOn.performHapticFeedback( - HapticFeedbackConstants.KEYBOARD_TAP, - HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); - } + if (mSettingsValues.mKeypressVibrationDuration >= 0) { + vibrate(mSettingsValues.mKeypressVibrationDuration); return; } - vibrate(mSettingsValues.mKeypressVibrationDuration); + // Go ahead with the system default + if (viewToPerformHapticFeedbackOn != null) { + viewToPerformHapticFeedbackOn.performHapticFeedback( + HapticFeedbackConstants.KEYBOARD_TAP, + HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); + } } public void onSettingsChanged(final SettingsValues settingsValues) { diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java index fd296988e..1b5791809 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java @@ -17,19 +17,27 @@ package com.android.inputmethod.latin; import android.text.TextUtils; +import android.util.Log; import android.util.SparseArray; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; -import com.android.inputmethod.latin.settings.NativeSuggestOptions; -import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.latin.makedict.DictionaryHeader; +import com.android.inputmethod.latin.makedict.FormatSpec; +import com.android.inputmethod.latin.makedict.FormatSpec.DictionaryOptions; +import com.android.inputmethod.latin.makedict.UnsupportedFormatException; +import com.android.inputmethod.latin.makedict.WordProperty; +import com.android.inputmethod.latin.utils.BinaryDictionaryUtils; +import com.android.inputmethod.latin.utils.FileUtils; import com.android.inputmethod.latin.utils.JniUtils; +import com.android.inputmethod.latin.utils.LanguageModelParam; import com.android.inputmethod.latin.utils.StringUtils; import java.io.File; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.Locale; import java.util.Map; @@ -40,10 +48,6 @@ import java.util.Map; public final class BinaryDictionary extends Dictionary { private static final String TAG = BinaryDictionary.class.getSimpleName(); - // Must be equal to MAX_WORD_LENGTH in native/jni/src/defines.h - private static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH; - // Must be equal to MAX_RESULTS in native/jni/src/defines.h - private static final int MAX_RESULTS = 18; // The cutoff returned by native for auto-commit confidence. // Must be equal to CONFIDENCE_TO_AUTO_COMMIT in native/jni/src/defines.h private static final int CONFIDENCE_TO_AUTO_COMMIT = 1000000; @@ -57,22 +61,34 @@ public final class BinaryDictionary extends Dictionary { @UsedForTesting public static final String MAX_BIGRAM_COUNT_QUERY = "MAX_BIGRAM_COUNT"; + public static final int NOT_A_VALID_TIMESTAMP = -1; + + // Format to get unigram flags from native side via getWordPropertyNative(). + private static final int FORMAT_WORD_PROPERTY_OUTPUT_FLAG_COUNT = 5; + private static final int FORMAT_WORD_PROPERTY_IS_NOT_A_WORD_INDEX = 0; + private static final int FORMAT_WORD_PROPERTY_IS_BLACKLISTED_INDEX = 1; + private static final int FORMAT_WORD_PROPERTY_HAS_BIGRAMS_INDEX = 2; + private static final int FORMAT_WORD_PROPERTY_HAS_SHORTCUTS_INDEX = 3; + private static final int FORMAT_WORD_PROPERTY_IS_BEGINNING_OF_SENTENCE_INDEX = 4; + + // Format to get probability and historical info from native side via getWordPropertyNative(). + public static final int FORMAT_WORD_PROPERTY_OUTPUT_PROBABILITY_INFO_COUNT = 4; + public static final int FORMAT_WORD_PROPERTY_PROBABILITY_INDEX = 0; + public static final int FORMAT_WORD_PROPERTY_TIMESTAMP_INDEX = 1; + public static final int FORMAT_WORD_PROPERTY_LEVEL_INDEX = 2; + public static final int FORMAT_WORD_PROPERTY_COUNT_INDEX = 3; + + public static final String DICT_FILE_NAME_SUFFIX_FOR_MIGRATION = ".migrate"; + private long mNativeDict; private final Locale mLocale; private final long mDictSize; private final String mDictFilePath; - private final int[] mInputCodePoints = new int[MAX_WORD_LENGTH]; - private final int[] mOutputCodePoints = new int[MAX_WORD_LENGTH * MAX_RESULTS]; - private final int[] mSpaceIndices = new int[MAX_RESULTS]; - private final int[] mOutputScores = new int[MAX_RESULTS]; - private final int[] mOutputTypes = new int[MAX_RESULTS]; - // Only one result is ever used - private final int[] mOutputAutoCommitFirstWordConfidence = new int[1]; + private final boolean mUseFullEditDistance; + private final boolean mIsUpdatable; + private boolean mHasUpdated; - private final NativeSuggestOptions mNativeSuggestOptions = new NativeSuggestOptions(); - - private final SparseArray<DicTraverseSession> mDicTraverseSessions = - CollectionUtils.newSparseArray(); + private final SparseArray<DicTraverseSession> mDicTraverseSessions = new SparseArray<>(); // TODO: There should be a way to remove used DicTraverseSession objects from // {@code mDicTraverseSessions}. @@ -80,19 +96,15 @@ public final class BinaryDictionary extends Dictionary { synchronized(mDicTraverseSessions) { DicTraverseSession traverseSession = mDicTraverseSessions.get(traverseSessionId); if (traverseSession == null) { - traverseSession = mDicTraverseSessions.get(traverseSessionId); - if (traverseSession == null) { - traverseSession = new DicTraverseSession(mLocale, mNativeDict, mDictSize); - mDicTraverseSessions.put(traverseSessionId, traverseSession); - } + traverseSession = new DicTraverseSession(mLocale, mNativeDict, mDictSize); + mDicTraverseSessions.put(traverseSessionId, traverseSession); } return traverseSession; } } /** - * Constructor for the binary dictionary. This is supposed to be called from the - * dictionary factory. + * Constructs binary dictionary using existing dictionary file. * @param filename the name of the file to read through native code. * @param offset the offset of the dictionary data within the file. * @param length the length of the binary data. @@ -107,125 +119,201 @@ public final class BinaryDictionary extends Dictionary { mLocale = locale; mDictSize = length; mDictFilePath = filename; - mNativeSuggestOptions.setUseFullEditDistance(useFullEditDistance); + mIsUpdatable = isUpdatable; + mHasUpdated = false; + mUseFullEditDistance = useFullEditDistance; loadDictionary(filename, offset, length, isUpdatable); } + /** + * Constructs binary dictionary on memory. + * @param filename the name of the file used to flush. + * @param useFullEditDistance whether to use the full edit distance in suggestions + * @param dictType the dictionary type, as a human-readable string + * @param formatVersion the format version of the dictionary + * @param attributeMap the attributes of the dictionary + */ + public BinaryDictionary(final String filename, final boolean useFullEditDistance, + final Locale locale, final String dictType, final long formatVersion, + final Map<String, String> attributeMap) { + super(dictType); + mLocale = locale; + mDictSize = 0; + mDictFilePath = filename; + // On memory dictionary is always updatable. + mIsUpdatable = true; + mHasUpdated = false; + mUseFullEditDistance = useFullEditDistance; + final String[] keyArray = new String[attributeMap.size()]; + final String[] valueArray = new String[attributeMap.size()]; + int index = 0; + for (final String key : attributeMap.keySet()) { + keyArray[index] = key; + valueArray[index] = attributeMap.get(key); + index++; + } + mNativeDict = createOnMemoryNative(formatVersion, locale.toString(), keyArray, valueArray); + } + + static { JniUtils.loadNativeLibrary(); } - private static native boolean createEmptyDictFileNative(String filePath, long dictVersion, - String[] attributeKeyStringArray, String[] attributeValueStringArray); private static native long openNative(String sourceDir, long dictOffset, long dictSize, boolean isUpdatable); - private static native void flushNative(long dict, String filePath); + private static native long createOnMemoryNative(long formatVersion, + String locale, String[] attributeKeyStringArray, String[] attributeValueStringArray); + private static native void getHeaderInfoNative(long dict, int[] outHeaderSize, + int[] outFormatVersion, ArrayList<int[]> outAttributeKeys, + ArrayList<int[]> outAttributeValues); + private static native boolean flushNative(long dict, String filePath); private static native boolean needsToRunGCNative(long dict, boolean mindsBlockByGC); - private static native void flushWithGCNative(long dict, String filePath); + private static native boolean flushWithGCNative(long dict, String filePath); private static native void closeNative(long dict); + private static native int getFormatVersionNative(long dict); private static native int getProbabilityNative(long dict, int[] word); - private static native int getBigramProbabilityNative(long dict, int[] word0, int[] word1); - private static native int getSuggestionsNative(long dict, long proximityInfo, + private static native int getMaxProbabilityOfExactMatchesNative(long dict, int[] word); + private static native int getNgramProbabilityNative(long dict, int[][] prevWordCodePointArrays, + boolean[] isBeginningOfSentenceArray, int[] word); + private static native void getWordPropertyNative(long dict, int[] word, + boolean isBeginningOfSentence, int[] outCodePoints, boolean[] outFlags, + int[] outProbabilityInfo, ArrayList<int[]> outBigramTargets, + ArrayList<int[]> outBigramProbabilityInfo, ArrayList<int[]> outShortcutTargets, + ArrayList<Integer> outShortcutProbabilities); + private static native int getNextWordNative(long dict, int token, int[] outCodePoints, + boolean[] outIsBeginningOfSentence); + private static native void getSuggestionsNative(long dict, long proximityInfo, long traverseSession, int[] xCoordinates, int[] yCoordinates, int[] times, - int[] pointerIds, int[] inputCodePoints, int inputSize, int commitPoint, - int[] suggestOptions, int[] prevWordCodePointArray, - int[] outputCodePoints, int[] outputScores, int[] outputIndices, int[] outputTypes, - int[] outputAutoCommitFirstWordConfidence); - private static native float calcNormalizedScoreNative(int[] before, int[] after, int score); - private static native int editDistanceNative(int[] before, int[] after); - private static native void addUnigramWordNative(long dict, int[] word, int probability); - private static native void addBigramWordsNative(long dict, int[] word0, int[] word1, - int probability); - private static native void removeBigramWordsNative(long dict, int[] word0, int[] word1); - private static native int calculateProbabilityNative(long dict, int unigramProbability, - int bigramProbability); + int[] pointerIds, int[] inputCodePoints, int inputSize, int[] suggestOptions, + int[][] prevWordCodePointArrays, boolean[] isBeginningOfSentenceArray, + int[] outputSuggestionCount, int[] outputCodePoints, int[] outputScores, + int[] outputIndices, int[] outputTypes, int[] outputAutoCommitFirstWordConfidence, + float[] inOutLanguageWeight); + private static native boolean addUnigramEntryNative(long dict, int[] word, int probability, + int[] shortcutTarget, int shortcutProbability, boolean isBeginningOfSentence, + boolean isNotAWord, boolean isBlacklisted, int timestamp); + private static native boolean removeUnigramEntryNative(long dict, int[] word); + private static native boolean addNgramEntryNative(long dict, + int[][] prevWordCodePointArrays, boolean[] isBeginningOfSentenceArray, + int[] word, int probability, int timestamp); + private static native boolean removeNgramEntryNative(long dict, + int[][] prevWordCodePointArrays, boolean[] isBeginningOfSentenceArray, int[] word); + private static native int addMultipleDictionaryEntriesNative(long dict, + LanguageModelParam[] languageModelParams, int startIndex); private static native String getPropertyNative(long dict, String query); - - @UsedForTesting - public static boolean createEmptyDictFile(final String filePath, final long dictVersion, - final Map<String, String> attributeMap) { - final String[] keyArray = new String[attributeMap.size()]; - final String[] valueArray = new String[attributeMap.size()]; - int index = 0; - for (final String key : attributeMap.keySet()) { - keyArray[index] = key; - valueArray[index] = attributeMap.get(key); - index++; - } - return createEmptyDictFileNative(filePath, dictVersion, keyArray, valueArray); - } + private static native boolean isCorruptedNative(long dict); + private static native boolean migrateNative(long dict, String dictFilePath, + long newFormatVersion); // TODO: Move native dict into session private final void loadDictionary(final String path, final long startOffset, final long length, final boolean isUpdatable) { + mHasUpdated = false; mNativeDict = openNative(path, startOffset, length, isUpdatable); } - @Override - public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, - final String prevWord, final ProximityInfo proximityInfo, - final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) { - return getSuggestionsWithSessionId(composer, prevWord, proximityInfo, blockOffensiveWords, - additionalFeaturesOptions, 0 /* sessionId */); + // TODO: Check isCorrupted() for main dictionaries. + public boolean isCorrupted() { + if (!isValidDictionary()) { + return false; + } + if (!isCorruptedNative(mNativeDict)) { + return false; + } + // TODO: Record the corruption. + Log.e(TAG, "BinaryDictionary (" + mDictFilePath + ") is corrupted."); + Log.e(TAG, "locale: " + mLocale); + Log.e(TAG, "dict size: " + mDictSize); + Log.e(TAG, "updatable: " + mIsUpdatable); + return true; + } + + public DictionaryHeader getHeader() throws UnsupportedFormatException { + if (mNativeDict == 0) { + return null; + } + final int[] outHeaderSize = new int[1]; + final int[] outFormatVersion = new int[1]; + final ArrayList<int[]> outAttributeKeys = new ArrayList<>(); + final ArrayList<int[]> outAttributeValues = new ArrayList<>(); + getHeaderInfoNative(mNativeDict, outHeaderSize, outFormatVersion, outAttributeKeys, + outAttributeValues); + final HashMap<String, String> attributes = new HashMap<>(); + for (int i = 0; i < outAttributeKeys.size(); i++) { + final String attributeKey = StringUtils.getStringFromNullTerminatedCodePointArray( + outAttributeKeys.get(i)); + final String attributeValue = StringUtils.getStringFromNullTerminatedCodePointArray( + outAttributeValues.get(i)); + attributes.put(attributeKey, attributeValue); + } + final boolean hasHistoricalInfo = DictionaryHeader.ATTRIBUTE_VALUE_TRUE.equals( + attributes.get(DictionaryHeader.HAS_HISTORICAL_INFO_KEY)); + return new DictionaryHeader(outHeaderSize[0], new DictionaryOptions(attributes), + new FormatSpec.FormatOptions(outFormatVersion[0], hasHistoricalInfo)); } @Override - public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer, - final String prevWord, final ProximityInfo proximityInfo, + public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, + final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, - final 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); - final int composerSize = composer.size(); - + final int sessionId, final float[] inOutLanguageWeight) { + if (!isValidDictionary()) { + return null; + } + final DicTraverseSession session = getTraverseSession(sessionId); + Arrays.fill(session.mInputCodePoints, Constants.NOT_A_CODE); + prevWordsInfo.outputToArray(session.mPrevWordCodePointArrays, + session.mIsBeginningOfSentenceArray); + final InputPointers inputPointers = composer.getInputPointers(); final boolean isGesture = composer.isBatchMode(); - if (composerSize <= 1 || !isGesture) { - if (composerSize > MAX_WORD_LENGTH - 1) return null; - for (int i = 0; i < composerSize; i++) { - mInputCodePoints[i] = composer.getCodeAt(i); + final int inputSize; + if (!isGesture) { + inputSize = composer.copyCodePointsExceptTrailingSingleQuotesAndReturnCodePointCount( + session.mInputCodePoints); + if (inputSize < 0) { + return null; } + } else { + inputSize = inputPointers.getPointerSize(); } - - final InputPointers ips = composer.getInputPointers(); - final int inputSize = isGesture ? ips.getPointerSize() : composerSize; - mNativeSuggestOptions.setIsGesture(isGesture); - mNativeSuggestOptions.setAdditionalFeaturesOptions(additionalFeaturesOptions); - // proximityInfo and/or prevWordForBigrams may not be null. - final int count = getSuggestionsNative(mNativeDict, proximityInfo.getNativeProximityInfo(), - getTraverseSession(sessionId).getSession(), ips.getXCoordinates(), - ips.getYCoordinates(), ips.getTimes(), ips.getPointerIds(), mInputCodePoints, - inputSize, 0 /* commitPoint */, mNativeSuggestOptions.getOptions(), - prevWordCodePointArray, mOutputCodePoints, mOutputScores, mSpaceIndices, - mOutputTypes, mOutputAutoCommitFirstWordConfidence); - final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList(); + session.mNativeSuggestOptions.setUseFullEditDistance(mUseFullEditDistance); + session.mNativeSuggestOptions.setIsGesture(isGesture); + session.mNativeSuggestOptions.setBlockOffensiveWords(blockOffensiveWords); + session.mNativeSuggestOptions.setAdditionalFeaturesOptions(additionalFeaturesOptions); + if (inOutLanguageWeight != null) { + session.mInputOutputLanguageWeight[0] = inOutLanguageWeight[0]; + } else { + session.mInputOutputLanguageWeight[0] = Dictionary.NOT_A_LANGUAGE_WEIGHT; + } + // TOOD: Pass multiple previous words information for n-gram. + getSuggestionsNative(mNativeDict, proximityInfo.getNativeProximityInfo(), + getTraverseSession(sessionId).getSession(), inputPointers.getXCoordinates(), + inputPointers.getYCoordinates(), inputPointers.getTimes(), + inputPointers.getPointerIds(), session.mInputCodePoints, inputSize, + session.mNativeSuggestOptions.getOptions(), session.mPrevWordCodePointArrays, + session.mIsBeginningOfSentenceArray, session.mOutputSuggestionCount, + session.mOutputCodePoints, session.mOutputScores, session.mSpaceIndices, + session.mOutputTypes, session.mOutputAutoCommitFirstWordConfidence, + session.mInputOutputLanguageWeight); + if (inOutLanguageWeight != null) { + inOutLanguageWeight[0] = session.mInputOutputLanguageWeight[0]; + } + final int count = session.mOutputSuggestionCount[0]; + final ArrayList<SuggestedWordInfo> suggestions = new ArrayList<>(); for (int j = 0; j < count; ++j) { - final int start = j * MAX_WORD_LENGTH; + final int start = j * Constants.DICTIONARY_MAX_WORD_LENGTH; int len = 0; - while (len < MAX_WORD_LENGTH && mOutputCodePoints[start + len] != 0) { + while (len < Constants.DICTIONARY_MAX_WORD_LENGTH + && session.mOutputCodePoints[start + len] != 0) { ++len; } if (len > 0) { - final int flags = mOutputTypes[j] & SuggestedWordInfo.KIND_MASK_FLAGS; - if (blockOffensiveWords - && 0 != (flags & SuggestedWordInfo.KIND_FLAG_POSSIBLY_OFFENSIVE) - && 0 == (flags & SuggestedWordInfo.KIND_FLAG_EXACT_MATCH)) { - // If we block potentially offensive words, and if the word is possibly - // offensive, then we don't output it unless it's also an exact match. - continue; - } - final int kind = mOutputTypes[j] & SuggestedWordInfo.KIND_MASK_KIND; - final int score = SuggestedWordInfo.KIND_WHITELIST == kind - ? SuggestedWordInfo.MAX_SCORE : mOutputScores[j]; - // TODO: check that all users of the `kind' parameter are ready to accept - // flags too and pass mOutputTypes[j] instead of kind - suggestions.add(new SuggestedWordInfo(new String(mOutputCodePoints, start, len), - score, kind, this /* sourceDict */, - mSpaceIndices[j] /* indexOfTouchPointOfSecondWord */, - mOutputAutoCommitFirstWordConfidence[0])); + suggestions.add(new SuggestedWordInfo( + new String(session.mOutputCodePoints, start, len), + session.mOutputScores[j], session.mOutputTypes[j], this /* sourceDict */, + session.mSpaceIndices[j] /* indexOfTouchPointOfSecondWord */, + session.mOutputAutoCommitFirstWordConfidence[0])); } } return suggestions; @@ -235,91 +323,220 @@ public final class BinaryDictionary extends Dictionary { return mNativeDict != 0; } - public static float calcNormalizedScore(final String before, final String after, - final int score) { - return calcNormalizedScoreNative(StringUtils.toCodePointArray(before), - StringUtils.toCodePointArray(after), score); - } - - public static int editDistance(final String before, final String after) { - if (before == null || after == null) { - throw new IllegalArgumentException(); - } - return editDistanceNative(StringUtils.toCodePointArray(before), - StringUtils.toCodePointArray(after)); + public int getFormatVersion() { + return getFormatVersionNative(mNativeDict); } @Override - public boolean isValidWord(final String word) { + public boolean isInDictionary(final String word) { return getFrequency(word) != NOT_A_PROBABILITY; } @Override public int getFrequency(final String word) { - if (word == null) return NOT_A_PROBABILITY; + if (TextUtils.isEmpty(word)) return NOT_A_PROBABILITY; int[] codePoints = StringUtils.toCodePointArray(word); return getProbabilityNative(mNativeDict, codePoints); } - // TODO: Add a batch process version (isValidBigramMultiple?) to avoid excessive numbers of jni - // calls when checking for changes in an entire dictionary. - public boolean isValidBigram(final String word0, final String word1) { - return getBigramProbability(word0, word1) != NOT_A_PROBABILITY; + @Override + public int getMaxFrequencyOfExactMatches(final String word) { + if (TextUtils.isEmpty(word)) return NOT_A_PROBABILITY; + int[] codePoints = StringUtils.toCodePointArray(word); + return getMaxProbabilityOfExactMatchesNative(mNativeDict, codePoints); + } + + @UsedForTesting + public boolean isValidNgram(final PrevWordsInfo prevWordsInfo, final String word) { + return getNgramProbability(prevWordsInfo, word) != NOT_A_PROBABILITY; } - public int getBigramProbability(final String word0, final String word1) { - if (TextUtils.isEmpty(word0) || TextUtils.isEmpty(word1)) return NOT_A_PROBABILITY; - final int[] codePoints0 = StringUtils.toCodePointArray(word0); - final int[] codePoints1 = StringUtils.toCodePointArray(word1); - return getBigramProbabilityNative(mNativeDict, codePoints0, codePoints1); + public int getNgramProbability(final PrevWordsInfo prevWordsInfo, final String word) { + if (!prevWordsInfo.isValid() || TextUtils.isEmpty(word)) { + return NOT_A_PROBABILITY; + } + final int[][] prevWordCodePointArrays = new int[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM][]; + final boolean[] isBeginningOfSentenceArray = + new boolean[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM]; + prevWordsInfo.outputToArray(prevWordCodePointArrays, isBeginningOfSentenceArray); + final int[] wordCodePoints = StringUtils.toCodePointArray(word); + return getNgramProbabilityNative(mNativeDict, prevWordCodePointArrays, + isBeginningOfSentenceArray, wordCodePoints); } - // Add a unigram entry to binary dictionary in native code. - public void addUnigramWord(final String word, final int probability) { + public WordProperty getWordProperty(final String word, final boolean isBeginningOfSentence) { + if (word == null) { + return null; + } + final int[] codePoints = StringUtils.toCodePointArray(word); + final int[] outCodePoints = new int[Constants.DICTIONARY_MAX_WORD_LENGTH]; + final boolean[] outFlags = new boolean[FORMAT_WORD_PROPERTY_OUTPUT_FLAG_COUNT]; + final int[] outProbabilityInfo = + new int[FORMAT_WORD_PROPERTY_OUTPUT_PROBABILITY_INFO_COUNT]; + final ArrayList<int[]> outBigramTargets = new ArrayList<>(); + final ArrayList<int[]> outBigramProbabilityInfo = new ArrayList<>(); + final ArrayList<int[]> outShortcutTargets = new ArrayList<>(); + final ArrayList<Integer> outShortcutProbabilities = new ArrayList<>(); + getWordPropertyNative(mNativeDict, codePoints, isBeginningOfSentence, outCodePoints, + outFlags, outProbabilityInfo, outBigramTargets, outBigramProbabilityInfo, + outShortcutTargets, outShortcutProbabilities); + return new WordProperty(codePoints, + outFlags[FORMAT_WORD_PROPERTY_IS_NOT_A_WORD_INDEX], + outFlags[FORMAT_WORD_PROPERTY_IS_BLACKLISTED_INDEX], + outFlags[FORMAT_WORD_PROPERTY_HAS_BIGRAMS_INDEX], + outFlags[FORMAT_WORD_PROPERTY_HAS_SHORTCUTS_INDEX], + outFlags[FORMAT_WORD_PROPERTY_IS_BEGINNING_OF_SENTENCE_INDEX], outProbabilityInfo, + outBigramTargets, outBigramProbabilityInfo, outShortcutTargets, + outShortcutProbabilities); + } + + public static class GetNextWordPropertyResult { + public WordProperty mWordProperty; + public int mNextToken; + + public GetNextWordPropertyResult(final WordProperty wordProperty, final int nextToken) { + mWordProperty = wordProperty; + mNextToken = nextToken; + } + } + + /** + * Method to iterate all words in the dictionary for makedict. + * If token is 0, this method newly starts iterating the dictionary. + */ + public GetNextWordPropertyResult getNextWordProperty(final int token) { + final int[] codePoints = new int[Constants.DICTIONARY_MAX_WORD_LENGTH]; + final boolean[] isBeginningOfSentence = new boolean[1]; + final int nextToken = getNextWordNative(mNativeDict, token, codePoints, + isBeginningOfSentence); + final String word = StringUtils.getStringFromNullTerminatedCodePointArray(codePoints); + return new GetNextWordPropertyResult( + getWordProperty(word, isBeginningOfSentence[0]), nextToken); + } + + // Add a unigram entry to binary dictionary with unigram attributes in native code. + public boolean addUnigramEntry(final String word, final int probability, + final String shortcutTarget, final int shortcutProbability, + final boolean isBeginningOfSentence, final boolean isNotAWord, + final boolean isBlacklisted, final int timestamp) { + if (word == null || (word.isEmpty() && !isBeginningOfSentence)) { + return false; + } + final int[] codePoints = StringUtils.toCodePointArray(word); + final int[] shortcutTargetCodePoints = (shortcutTarget != null) ? + StringUtils.toCodePointArray(shortcutTarget) : null; + if (!addUnigramEntryNative(mNativeDict, codePoints, probability, shortcutTargetCodePoints, + shortcutProbability, isBeginningOfSentence, isNotAWord, isBlacklisted, timestamp)) { + return false; + } + mHasUpdated = true; + return true; + } + + // Remove a unigram entry from the binary dictionary in native code. + public boolean removeUnigramEntry(final String word) { if (TextUtils.isEmpty(word)) { - return; + return false; } final int[] codePoints = StringUtils.toCodePointArray(word); - addUnigramWordNative(mNativeDict, codePoints, probability); + if (!removeUnigramEntryNative(mNativeDict, codePoints)) { + return false; + } + mHasUpdated = true; + return true; } - // Add a bigram entry to binary dictionary in native code. - public void addBigramWords(final String word0, final String word1, final int probability) { - if (TextUtils.isEmpty(word0) || TextUtils.isEmpty(word1)) { - return; + // Add an n-gram entry to the binary dictionary with timestamp in native code. + public boolean addNgramEntry(final PrevWordsInfo prevWordsInfo, final String word, + final int probability, final int timestamp) { + if (!prevWordsInfo.isValid() || TextUtils.isEmpty(word)) { + return false; } - final int[] codePoints0 = StringUtils.toCodePointArray(word0); - final int[] codePoints1 = StringUtils.toCodePointArray(word1); - addBigramWordsNative(mNativeDict, codePoints0, codePoints1, probability); + final int[][] prevWordCodePointArrays = new int[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM][]; + final boolean[] isBeginningOfSentenceArray = + new boolean[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM]; + prevWordsInfo.outputToArray(prevWordCodePointArrays, isBeginningOfSentenceArray); + final int[] wordCodePoints = StringUtils.toCodePointArray(word); + if (!addNgramEntryNative(mNativeDict, prevWordCodePointArrays, + isBeginningOfSentenceArray, wordCodePoints, probability, timestamp)) { + return false; + } + mHasUpdated = true; + return true; } - // Remove a bigram entry form binary dictionary in native code. - public void removeBigramWords(final String word0, final String word1) { - if (TextUtils.isEmpty(word0) || TextUtils.isEmpty(word1)) { - return; + // Remove an n-gram entry from the binary dictionary in native code. + public boolean removeNgramEntry(final PrevWordsInfo prevWordsInfo, final String word) { + if (!prevWordsInfo.isValid() || TextUtils.isEmpty(word)) { + return false; + } + final int[][] prevWordCodePointArrays = new int[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM][]; + final boolean[] isBeginningOfSentenceArray = + new boolean[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM]; + prevWordsInfo.outputToArray(prevWordCodePointArrays, isBeginningOfSentenceArray); + final int[] wordCodePoints = StringUtils.toCodePointArray(word); + if (!removeNgramEntryNative(mNativeDict, prevWordCodePointArrays, + isBeginningOfSentenceArray, wordCodePoints)) { + return false; + } + mHasUpdated = true; + return true; + } + + public void addMultipleDictionaryEntries(final LanguageModelParam[] languageModelParams) { + if (!isValidDictionary()) return; + int processedParamCount = 0; + while (processedParamCount < languageModelParams.length) { + if (needsToRunGC(true /* mindsBlockByGC */)) { + flushWithGC(); + } + processedParamCount = addMultipleDictionaryEntriesNative(mNativeDict, + languageModelParams, processedParamCount); + mHasUpdated = true; + if (processedParamCount <= 0) { + return; + } } - final int[] codePoints0 = StringUtils.toCodePointArray(word0); - final int[] codePoints1 = StringUtils.toCodePointArray(word1); - removeBigramWordsNative(mNativeDict, codePoints0, codePoints1); } private void reopen() { close(); final File dictFile = new File(mDictFilePath); - mNativeDict = openNative(dictFile.getAbsolutePath(), 0 /* startOffset */, - dictFile.length(), true /* isUpdatable */); + // WARNING: Because we pass 0 as the offset and file.length() as the length, this can + // only be called for actual files. Right now it's only called by the flush() family of + // functions, which require an updatable dictionary, so it's okay. But beware. + loadDictionary(dictFile.getAbsolutePath(), 0 /* startOffset */, + dictFile.length(), mIsUpdatable); } - public void flush() { - if (!isValidDictionary()) return; - flushNative(mNativeDict, mDictFilePath); - reopen(); + // Flush to dict file if the dictionary has been updated. + public boolean flush() { + if (!isValidDictionary()) return false; + if (mHasUpdated) { + if (!flushNative(mNativeDict, mDictFilePath)) { + return false; + } + reopen(); + } + return true; } - public void flushWithGC() { - if (!isValidDictionary()) return; - flushWithGCNative(mNativeDict, mDictFilePath); + // Run GC and flush to dict file if the dictionary has been updated. + public boolean flushWithGCIfHasUpdated() { + if (mHasUpdated) { + return flushWithGC(); + } + return true; + } + + // Run GC and flush to dict file. + public boolean flushWithGC() { + if (!isValidDictionary()) return false; + if (!flushWithGCNative(mNativeDict, mDictFilePath)) { + return false; + } reopen(); + return true; } /** @@ -333,14 +550,30 @@ public final class BinaryDictionary extends Dictionary { return needsToRunGCNative(mNativeDict, mindsBlockByGC); } - @UsedForTesting - public int calculateProbability(final int unigramProbability, final int bigramProbability) { - if (!isValidDictionary()) return NOT_A_PROBABILITY; - return calculateProbabilityNative(mNativeDict, unigramProbability, bigramProbability); + public boolean migrateTo(final int newFormatVersion) { + if (!isValidDictionary()) { + return false; + } + final String tmpDictFilePath = mDictFilePath + DICT_FILE_NAME_SUFFIX_FOR_MIGRATION; + if (!migrateNative(mNativeDict, tmpDictFilePath, newFormatVersion)) { + return false; + } + close(); + final File dictFile = new File(mDictFilePath); + final File tmpDictFile = new File(tmpDictFilePath); + if (!FileUtils.deleteRecursively(dictFile)) { + return false; + } + if (!BinaryDictionaryUtils.renameDict(tmpDictFile, dictFile)) { + return false; + } + loadDictionary(dictFile.getAbsolutePath(), 0 /* startOffset */, + dictFile.length(), mIsUpdatable); + return true; } @UsedForTesting - public String getPropertyForTests(String query) { + public String getPropertyForTest(final String query) { if (!isValidDictionary()) return ""; return getPropertyNative(mNativeDict, query); } diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java index 722a82961..10b1f1b77 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java @@ -28,7 +28,7 @@ import android.text.TextUtils; import android.util.Log; import com.android.inputmethod.dictionarypack.DictionaryPackConstants; -import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.dictionarypack.MD5Calculator; import com.android.inputmethod.latin.utils.DictionaryInfoUtils; import com.android.inputmethod.latin.utils.DictionaryInfoUtils.DictionaryInfo; import com.android.inputmethod.latin.utils.FileTransforms; @@ -38,12 +38,13 @@ import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.Closeable; import java.io.File; +import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.util.Arrays; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -98,7 +99,7 @@ public final class BinaryDictionaryFileDumper { * This creates a URI builder able to build a URI pointing to the dictionary * pack content provider for a specific dictionary id. */ - private static Uri.Builder getProviderUriBuilder(final String path) { + public static Uri.Builder getProviderUriBuilder(final String path) { return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) .authority(DictionaryPackConstants.AUTHORITY).appendPath(path); } @@ -142,7 +143,7 @@ public final class BinaryDictionaryFileDumper { final ContentProviderClient client = context.getContentResolver(). acquireContentProviderClient(getProviderUriBuilder("").build()); if (null == client) return Collections.<WordListInfo>emptyList(); - + Cursor cursor = null; try { final Uri.Builder builder = getContentUriBuilderForType(clientId, client, QUERY_PATH_DICT_INFO, locale.toString()); @@ -154,24 +155,23 @@ public final class BinaryDictionaryFileDumper { final boolean isProtocolV2 = (QUERY_PARAMETER_PROTOCOL_VALUE.equals( queryUri.getQueryParameter(QUERY_PARAMETER_PROTOCOL))); - Cursor c = client.query(queryUri, DICTIONARY_PROJECTION, null, null, null); - if (isProtocolV2 && null == c) { + cursor = client.query(queryUri, DICTIONARY_PROJECTION, null, null, null); + if (isProtocolV2 && null == cursor) { reinitializeClientRecordInDictionaryContentProvider(context, client, clientId); - c = client.query(queryUri, DICTIONARY_PROJECTION, null, null, null); + cursor = client.query(queryUri, DICTIONARY_PROJECTION, null, null, null); } - if (null == c) return Collections.<WordListInfo>emptyList(); - if (c.getCount() <= 0 || !c.moveToFirst()) { - c.close(); + if (null == cursor) return Collections.<WordListInfo>emptyList(); + if (cursor.getCount() <= 0 || !cursor.moveToFirst()) { return Collections.<WordListInfo>emptyList(); } - final ArrayList<WordListInfo> list = CollectionUtils.newArrayList(); + final ArrayList<WordListInfo> list = new ArrayList<>(); do { - final String wordListId = c.getString(0); - final String wordListLocale = c.getString(1); + final String wordListId = cursor.getString(0); + final String wordListLocale = cursor.getString(1); + final String wordListRawChecksum = cursor.getString(2); if (TextUtils.isEmpty(wordListId)) continue; - list.add(new WordListInfo(wordListId, wordListLocale)); - } while (c.moveToNext()); - c.close(); + list.add(new WordListInfo(wordListId, wordListLocale, wordListRawChecksum)); + } while (cursor.moveToNext()); return list; } catch (RemoteException e) { // The documentation is unclear as to in which cases this may happen, but it probably @@ -186,6 +186,9 @@ public final class BinaryDictionaryFileDumper { Log.e(TAG, "Unexpected exception communicating with the dictionary pack", e); return Collections.<WordListInfo>emptyList(); } finally { + if (null != cursor) { + cursor.close(); + } client.release(); } } @@ -216,7 +219,8 @@ public final class BinaryDictionaryFileDumper { * and creating it (and its containing directory) if necessary. */ private static void cacheWordList(final String wordlistId, final String locale, - final ContentProviderClient providerClient, final Context context) { + final String rawChecksum, final ContentProviderClient providerClient, + final Context context) { final int COMPRESSED_CRYPTED_COMPRESSED = 0; final int CRYPTED_COMPRESSED = 1; final int COMPRESSED_CRYPTED = 2; @@ -298,6 +302,13 @@ public final class BinaryDictionaryFileDumper { checkMagicAndCopyFileTo(bufferedInputStream, bufferedOutputStream); bufferedOutputStream.flush(); bufferedOutputStream.close(); + final String actualRawChecksum = MD5Calculator.checksum( + new BufferedInputStream(new FileInputStream(outputFile))); + Log.i(TAG, "Computed checksum for downloaded dictionary. Expected = " + rawChecksum + + " ; actual = " + actualRawChecksum); + if (!TextUtils.isEmpty(rawChecksum) && !rawChecksum.equals(actualRawChecksum)) { + throw new IOException("Could not decode the file correctly : checksum differs"); + } final File finalFile = new File(finalFileName); finalFile.delete(); if (!outputFile.renameTo(finalFile)) { @@ -339,15 +350,25 @@ public final class BinaryDictionaryFileDumper { Log.e(TAG, "Could not copy a word list. Will not be able to use it."); // If we can't copy it we should warn the dictionary provider so that it can mark it // as invalid. - wordListUriBuilder.appendQueryParameter(QUERY_PARAMETER_DELETE_RESULT, - QUERY_PARAMETER_FAILURE); + reportBrokenFileToDictionaryProvider(providerClient, clientId, wordlistId); + } + + public static boolean reportBrokenFileToDictionaryProvider( + final ContentProviderClient providerClient, final String clientId, + final String wordlistId) { try { + final Uri.Builder wordListUriBuilder = getContentUriBuilderForType(clientId, + providerClient, QUERY_PATH_DATAFILE, wordlistId /* extraPath */); + wordListUriBuilder.appendQueryParameter(QUERY_PARAMETER_DELETE_RESULT, + QUERY_PARAMETER_FAILURE); if (0 >= providerClient.delete(wordListUriBuilder.build(), null, null)) { - Log.e(TAG, "In addition, we were unable to delete it."); + Log.e(TAG, "Unable to delete a word list."); } } catch (RemoteException e) { - Log.e(TAG, "In addition, communication with the dictionary provider was cut", e); + Log.e(TAG, "Communication with the dictionary provider was cut", e); + return false; } + return true; } // Ideally the two following methods should be merged, but AssetFileDescriptor does not @@ -397,7 +418,7 @@ public final class BinaryDictionaryFileDumper { final List<WordListInfo> idList = getWordListWordListInfos(locale, context, hasDefaultWordList); for (WordListInfo id : idList) { - cacheWordList(id.mId, id.mLocale, providerClient, context); + cacheWordList(id.mId, id.mLocale, id.mRawChecksum, providerClient, context); } } finally { providerClient.release(); @@ -432,8 +453,9 @@ public final class BinaryDictionaryFileDumper { // Actually copy the file final byte[] buffer = new byte[FILE_READ_BUFFER_SIZE]; - for (int readBytes = input.read(buffer); readBytes >= 0; readBytes = input.read(buffer)) + for (int readBytes = input.read(buffer); readBytes >= 0; readBytes = input.read(buffer)) { output.write(buffer, 0, readBytes); + } input.close(); } @@ -478,8 +500,7 @@ public final class BinaryDictionaryFileDumper { * @param context the context for resources and providers. * @param clientId the client ID to use. */ - public static void initializeClientRecordHelper(final Context context, - final String clientId) { + public static void initializeClientRecordHelper(final Context context, final String clientId) { try { final ContentProviderClient client = context.getContentResolver(). acquireContentProviderClient(getProviderUriBuilder("").build()); diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java index 181ad17ea..867c18686 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java @@ -21,11 +21,9 @@ import android.content.SharedPreferences; import android.content.res.AssetFileDescriptor; import android.util.Log; -import com.android.inputmethod.latin.makedict.DictDecoder; -import com.android.inputmethod.latin.makedict.FormatSpec; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; +import com.android.inputmethod.latin.makedict.DictionaryHeader; import com.android.inputmethod.latin.makedict.UnsupportedFormatException; -import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.latin.utils.BinaryDictionaryUtils; import com.android.inputmethod.latin.utils.DictionaryInfoUtils; import com.android.inputmethod.latin.utils.LocaleUtils; @@ -112,7 +110,7 @@ final public class BinaryDictionaryGetter { public DictPackSettings(final Context context) { mDictPreferences = null == context ? null : context.getSharedPreferences(COMMON_PREFERENCES_NAME, - Context.MODE_WORLD_READABLE | Context.MODE_MULTI_PROCESS); + Context.MODE_MULTI_PROCESS); } public boolean isWordListActive(final String dictId) { if (null == mDictPreferences) { @@ -161,7 +159,7 @@ final public class BinaryDictionaryGetter { public static File[] getCachedWordLists(final String locale, final Context context) { final File[] directoryList = DictionaryInfoUtils.getCachedDirectoryList(context); if (null == directoryList) return EMPTY_FILE_ARRAY; - final HashMap<String, FileAndMatchLevel> cacheFiles = CollectionUtils.newHashMap(); + final HashMap<String, FileAndMatchLevel> cacheFiles = new HashMap<>(); for (File directory : directoryList) { if (!directory.isDirectory()) continue; final String dirLocale = @@ -226,12 +224,10 @@ final public class BinaryDictionaryGetter { // ## HACK ## we prevent usage of a dictionary before version 18. The reason for this is, since // those do not include whitelist entries, the new code with an old version of the dictionary // would lose whitelist functionality. - private static boolean hackCanUseDictionaryFile(final Locale locale, final File f) { + private static boolean hackCanUseDictionaryFile(final Locale locale, final File file) { try { // Read the version of the file - final DictDecoder dictDecoder = FormatSpec.getDictDecoder(f); - final FileHeader header = dictDecoder.readHeader(); - + final DictionaryHeader header = BinaryDictionaryUtils.getHeader(file); final String version = header.mDictionaryOptions.mAttributes.get(VERSION_KEY); if (null == version) { // No version in the options : the format is unexpected @@ -276,7 +272,7 @@ final public class BinaryDictionaryGetter { final DictPackSettings dictPackSettings = new DictPackSettings(context); boolean foundMainDict = false; - final ArrayList<AssetFileAddress> fileList = CollectionUtils.newArrayList(); + final ArrayList<AssetFileAddress> fileList = new ArrayList<>(); // cachedWordLists may not be null, see doc for getCachedDictionaryList for (final File f : cachedWordLists) { final String wordListId = DictionaryInfoUtils.getWordListIdFromFileName(f.getName()); diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java index 9a9653094..43af66eb7 100644 --- a/java/src/com/android/inputmethod/latin/Constants.java +++ b/java/src/com/android/inputmethod/latin/Constants.java @@ -70,42 +70,56 @@ public final class Constants { public static final class ExtraValue { /** - * The subtype extra value used to indicate that the subtype keyboard layout is capable - * for typing ASCII characters. + * The subtype extra value used to indicate that this subtype is capable of + * entering ASCII characters. */ public static final String ASCII_CAPABLE = "AsciiCapable"; /** - * The subtype extra value used to indicate that the subtype keyboard layout is capable - * for typing EMOJI characters. + * The subtype extra value used to indicate that this subtype is enabled + * when the default subtype is not marked as ascii capable. + */ + public static final String ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE = + "EnabledWhenDefaultIsNotAsciiCapable"; + + /** + * The subtype extra value used to indicate that this subtype is capable of + * entering emoji characters. */ public static final String EMOJI_CAPABLE = "EmojiCapable"; + /** - * The subtype extra value used to indicate that the subtype require network connection - * to work. + * The subtype extra value used to indicate that this subtype requires a network + * connection to work. */ public static final String REQ_NETWORK_CONNECTIVITY = "requireNetworkConnectivity"; /** - * The subtype extra value used to indicate that the subtype display name contains "%s" - * for replacement mark and it should be replaced by this extra value. + * The subtype extra value used to indicate that the display name of this subtype + * contains a "%s" for printf-like replacement and it should be replaced by + * this extra value. * This extra value is supported on JellyBean and later. */ public static final String UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME = "UntranslatableReplacementStringInSubtypeName"; /** - * The subtype extra value used to indicate that the subtype keyboard layout set name. + * The subtype extra value used to indicate this subtype keyboard layout set name. * This extra value is private to LatinIME. */ public static final String KEYBOARD_LAYOUT_SET = "KeyboardLayoutSet"; /** - * The subtype extra value used to indicate that the subtype is additional subtype + * The subtype extra value used to indicate that this subtype is an additional subtype * that the user defined. This extra value is private to LatinIME. */ public static final String IS_ADDITIONAL_SUBTYPE = "isAdditionalSubtype"; + /** + * The subtype extra value used to specify the combining rules. + */ + public static final String COMBINING_RULES = "CombiningRules"; + private ExtraValue() { // This utility class is not publicly instantiable. } @@ -124,6 +138,8 @@ public final class Constants { * {@link android.text.TextUtils#CAP_MODE_WORDS}, and * {@link android.text.TextUtils#CAP_MODE_SENTENCES}. */ + // TODO: Straighten this out. It's bizarre to have to use android.text.TextUtils.CAP_MODE_* + // except for OFF that is in Constants.TextUtils. public static final int CAP_MODE_OFF = 0; private TextUtils() { @@ -132,7 +148,8 @@ public final class Constants { } public static final int NOT_A_CODE = -1; - + public static final int NOT_A_CURSOR_POSITION = -1; + // TODO: replace the following constants with state in InputTransaction? public static final int NOT_A_COORDINATE = -1; public static final int SUGGESTION_STRIP_COORDINATE = -2; public static final int SPELL_CHECKER_COORDINATE = -3; @@ -141,10 +158,27 @@ public final class Constants { // A hint on how many characters to cache from the TextView. A good value of this is given by // how many characters we need to be able to almost always find the caps mode. public static final int EDITOR_CONTENTS_CACHE_SIZE = 1024; + // How many characters we accept for the recapitalization functionality. This needs to be + // large enough for all reasonable purposes, but avoid purposeful attacks. 100k sounds about + // right for this. + public static final int MAX_CHARACTERS_FOR_RECAPITALIZATION = 1024 * 100; // Must be equal to MAX_WORD_LENGTH in native/jni/src/defines.h public static final int DICTIONARY_MAX_WORD_LENGTH = 48; + // (MAX_PREV_WORD_COUNT_FOR_N_GRAM + 1)-gram is supported in Java side. Needs to modify + // MAX_PREV_WORD_COUNT_FOR_N_GRAM in native/jni/src/defines.h for suggestions. + public static final int MAX_PREV_WORD_COUNT_FOR_N_GRAM = 2; + + // Key events coming any faster than this are long-presses. + public static final int LONG_PRESS_MILLISECONDS = 200; + // TODO: Set this value appropriately. + public static final int GET_SUGGESTED_WORDS_TIMEOUT = 200; + // How many continuous deletes at which to start deleting at a higher speed. + public static final int DELETE_ACCELERATE_AT = 20; + + public static final String WORD_SEPARATOR = " "; + public static boolean isValidCoordinate(final int coordinate) { // Detect {@link NOT_A_COORDINATE}, {@link SUGGESTION_STRIP_COORDINATE}, // and {@link SPELL_CHECKER_COORDINATE}. @@ -165,13 +199,15 @@ public final class Constants { public static final int CODE_TAB = '\t'; public static final int CODE_SPACE = ' '; public static final int CODE_PERIOD = '.'; - public static final int CODE_ARMENIAN_PERIOD = 0x0589; + public static final int CODE_COMMA = ','; 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 = '!'; public static final int CODE_SLASH = '/'; + public static final int CODE_BACKSLASH = '\\'; + public static final int CODE_VERTICAL_BAR = '|'; public static final int CODE_COMMERCIAL_AT = '@'; public static final int CODE_PLUS = '+'; public static final int CODE_PERCENT = '%'; @@ -179,6 +215,12 @@ public final class Constants { public static final int CODE_CLOSING_SQUARE_BRACKET = ']'; public static final int CODE_CLOSING_CURLY_BRACKET = '}'; public static final int CODE_CLOSING_ANGLE_BRACKET = '>'; + public static final int CODE_INVERTED_QUESTION_MARK = 0xBF; // ¿ + public static final int CODE_INVERTED_EXCLAMATION_MARK = 0xA1; // ¡ + + public static final String REGEXP_PERIOD = "\\."; + public static final String STRING_SPACE = " "; + public static final String STRING_PERIOD_AND_SPACE = ". "; /** * Special keys code. Must be negative. @@ -197,8 +239,10 @@ public final class Constants { public static final int CODE_LANGUAGE_SWITCH = -10; public static final int CODE_EMOJI = -11; public static final int CODE_SHIFT_ENTER = -12; + public static final int CODE_SYMBOL_SHIFT = -13; + public static final int CODE_ALPHA_FROM_EMOJI = -14; // Code value representing the code is not specified. - public static final int CODE_UNSPECIFIED = -13; + public static final int CODE_UNSPECIFIED = -15; public static boolean isLetterCode(final int code) { return code >= CODE_SPACE; @@ -218,20 +262,50 @@ public final class Constants { case CODE_LANGUAGE_SWITCH: return "languageSwitch"; case CODE_EMOJI: return "emoji"; case CODE_SHIFT_ENTER: return "shiftEnter"; + case CODE_ALPHA_FROM_EMOJI: return "alpha"; case CODE_UNSPECIFIED: return "unspec"; case CODE_TAB: return "tab"; case CODE_ENTER: return "enter"; + case CODE_SPACE: return "space"; default: - if (code < CODE_SPACE) return String.format("'\\u%02x'", code); - if (code < 0x100) return String.format("'%c'", code); - return String.format("'\\u%04x'", code); + if (code < CODE_SPACE) return String.format("\\u%02X", code); + if (code < 0x100) return String.format("%c", code); + if (code < 0x10000) return String.format("\\u%04X", code); + return String.format("\\U%05X", code); + } + } + + public static String printableCodes(final int[] codes) { + final StringBuilder sb = new StringBuilder(); + boolean addDelimiter = false; + for (final int code : codes) { + if (code == NOT_A_CODE) break; + if (addDelimiter) sb.append(", "); + sb.append(printableCode(code)); + addDelimiter = true; } + return "[" + sb + "]"; } public static final int MAX_INT_BIT_COUNT = 32; + /** + * Screen metrics (a.k.a. Device form factor) constants of + * {@link R.integer#config_screen_metrics}. + */ + public static final int SCREEN_METRICS_SMALL_PHONE = 0; + public static final int SCREEN_METRICS_LARGE_PHONE = 1; + public static final int SCREEN_METRICS_LARGE_TABLET = 2; + public static final int SCREEN_METRICS_SMALL_TABLET = 3; + + /** + * Default capacity of gesture points container. + * This constant is used by {@link BatchInputArbiter} and etc. to preallocate regions that + * contain gesture event points. + */ + public static final int DEFAULT_GESTURE_POINTS_CAPACITY = 128; + private Constants() { // This utility class is not publicly instantiable. } - } diff --git a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java index 47891c6b7..ad14c06ef 100644 --- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java @@ -16,8 +16,6 @@ package com.android.inputmethod.latin; -import com.android.inputmethod.latin.personalization.AccountUtils; - import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; @@ -31,8 +29,13 @@ import android.provider.ContactsContract.Contacts; import android.text.TextUtils; import android.util.Log; +import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.personalization.AccountUtils; +import com.android.inputmethod.latin.utils.ExecutorUtils; import com.android.inputmethod.latin.utils.StringUtils; +import java.io.File; +import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -44,7 +47,8 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { private static final String TAG = ContactsBinaryDictionary.class.getSimpleName(); private static final String NAME = "contacts"; - private static boolean DEBUG = false; + private static final boolean DEBUG = false; + private static final boolean DEBUG_DUMP = false; /** * Frequency for contacts information into the dictionary @@ -58,10 +62,10 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { private static final int INDEX_NAME = 1; /** The number of contacts in the most recent dictionary rebuild. */ - static private int sContactCountAtLastRebuild = 0; + private int mContactCountAtLastRebuild = 0; - /** The locale for this contacts dictionary. Controls name bigram predictions. */ - public final Locale mLocale; + /** The hash code of ArrayList of contacts names in the most recent dictionary rebuild. */ + private int mHashCodeAtLastRebuild = 0; private ContentObserver mObserver; @@ -70,36 +74,40 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { */ private final boolean mUseFirstLastBigrams; - public ContactsBinaryDictionary(final Context context, final Locale locale) { - super(context, getFilenameWithLocale(NAME, locale.toString()), Dictionary.TYPE_CONTACTS, - false /* isUpdatable */); - mLocale = locale; + protected ContactsBinaryDictionary(final Context context, final Locale locale, + final File dictFile, final String name) { + super(context, getDictName(name, locale, dictFile), locale, Dictionary.TYPE_CONTACTS, + dictFile); mUseFirstLastBigrams = useFirstLastBigramsForLocale(locale); registerObserver(context); + reloadDictionaryIfRequired(); + } - // Load the current binary dictionary from internal storage. If no binary dictionary exists, - // loadDictionary will start a new thread to generate one asynchronously. - loadDictionary(); + @UsedForTesting + public static ContactsBinaryDictionary getDictionary(final Context context, final Locale locale, + final File dictFile, final String dictNamePrefix) { + return new ContactsBinaryDictionary(context, locale, dictFile, dictNamePrefix + NAME); } 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); + ExecutorUtils.getExecutor("Check Contacts").execute(new Runnable() { + @Override + public void run() { + if (haveContentsChanged()) { + setNeedsToRecreate(); + } + } + }); } }); } - public void reopen(final Context context) { - registerObserver(context); - } - @Override public synchronized void close() { if (mObserver != null) { @@ -110,14 +118,14 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { } @Override - public void loadDictionaryAsync() { - loadDeviceAccountsEmailAddresses(); - loadDictionaryAsyncForUri(ContactsContract.Profile.CONTENT_URI); + public void loadInitialContentsLocked() { + loadDeviceAccountsEmailAddressesLocked(); + loadDictionaryForUriLocked(ContactsContract.Profile.CONTENT_URI); // TODO: Switch this URL to the newer ContactsContract too - loadDictionaryAsyncForUri(Contacts.CONTENT_URI); + loadDictionaryForUriLocked(Contacts.CONTENT_URI); } - private void loadDeviceAccountsEmailAddresses() { + private void loadDeviceAccountsEmailAddressesLocked() { final List<String> accountVocabulary = AccountUtils.getDeviceAccountsEmailAddresses(mContext); if (accountVocabulary == null || accountVocabulary.isEmpty()) { @@ -127,29 +135,32 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { if (DEBUG) { Log.d(TAG, "loadAccountVocabulary: " + word); } - super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS, 0 /* shortcutFreq */, - false /* isNotAWord */); + runGCIfRequiredLocked(true /* mindsBlockByGC */); + addUnigramLocked(word, FREQUENCY_FOR_CONTACTS, null /* shortcut */, + 0 /* shortcutFreq */, false /* isNotAWord */, false /* isBlacklisted */, + BinaryDictionary.NOT_A_VALID_TIMESTAMP); } } - private void loadDictionaryAsyncForUri(final Uri uri) { + private void loadDictionaryForUriLocked(final Uri uri) { + Cursor cursor = null; try { - Cursor cursor = mContext.getContentResolver() - .query(uri, PROJECTION, null, null, null); - if (cursor != null) { - try { - if (cursor.moveToFirst()) { - sContactCountAtLastRebuild = getContactCount(); - addWords(cursor); - } - } finally { - cursor.close(); - } + cursor = mContext.getContentResolver().query(uri, PROJECTION, null, null, null); + if (null == cursor) { + return; + } + if (cursor.moveToFirst()) { + mContactCountAtLastRebuild = getContactCount(); + addWordsLocked(cursor); } } catch (final SQLiteException e) { Log.e(TAG, "SQLiteException in the remote Contacts process.", e); } catch (final IllegalStateException e) { Log.e(TAG, "Contacts DB is having problems", e); + } finally { + if (null != cursor) { + cursor.close(); + } } } @@ -161,33 +172,42 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { return false; } - private void addWords(final Cursor cursor) { + private void addWordsLocked(final Cursor cursor) { int count = 0; + final ArrayList<String> names = new ArrayList<>(); while (!cursor.isAfterLast() && count < MAX_CONTACT_COUNT) { String name = cursor.getString(INDEX_NAME); if (isValidName(name)) { - addName(name); + names.add(name); + addNameLocked(name); ++count; + } else { + if (DEBUG_DUMP) { + Log.d(TAG, "Invalid name: " + name); + } } cursor.moveToNext(); } + mHashCodeAtLastRebuild = names.hashCode(); } private int getContactCount() { // TODO: consider switching to a rawQuery("select count(*)...") on the database if // performance is a bottleneck. + Cursor cursor = null; try { - final Cursor cursor = mContext.getContentResolver().query( - Contacts.CONTENT_URI, PROJECTION_ID_ONLY, null, null, null); - if (cursor != null) { - try { - return cursor.getCount(); - } finally { - cursor.close(); - } + cursor = mContext.getContentResolver().query(Contacts.CONTENT_URI, PROJECTION_ID_ONLY, + null, null, null); + if (null == cursor) { + return 0; } + return cursor.getCount(); } catch (final SQLiteException e) { Log.e(TAG, "SQLiteException in the remote Contacts process.", e); + } finally { + if (null != cursor) { + cursor.close(); + } } return 0; } @@ -196,31 +216,36 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { * Adds the words in a name (e.g., firstname/lastname) to the binary dictionary along with their * bigrams depending on locale. */ - private void addName(final String name) { + private void addNameLocked(final String name) { int len = StringUtils.codePointCount(name); - String prevWord = null; + PrevWordsInfo prevWordsInfo = PrevWordsInfo.EMPTY_PREV_WORDS_INFO; // TODO: Better tokenization for non-Latin writing systems for (int i = 0; i < len; i++) { if (Character.isLetter(name.codePointAt(i))) { int end = getWordEndPosition(name, len, i); String word = name.substring(i, end); + if (DEBUG_DUMP) { + Log.d(TAG, "addName word = " + word); + } i = end - 1; // Don't add single letter words, possibly confuses // capitalization of i. final int wordLen = StringUtils.codePointCount(word); if (wordLen < MAX_WORD_LENGTH && wordLen > 1) { if (DEBUG) { - Log.d(TAG, "addName " + name + ", " + word + ", " + prevWord); + Log.d(TAG, "addName " + name + ", " + word + ", " + prevWordsInfo); } - super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS, - 0 /* shortcutFreq */, false /* isNotAWord */); - if (!TextUtils.isEmpty(prevWord)) { - if (mUseFirstLastBigrams) { - super.addBigram(prevWord, word, FREQUENCY_FOR_CONTACTS_BIGRAM, - 0 /* lastModifiedTime */); - } + runGCIfRequiredLocked(true /* mindsBlockByGC */); + addUnigramLocked(word, FREQUENCY_FOR_CONTACTS, + null /* shortcut */, 0 /* shortcutFreq */, false /* isNotAWord */, + false /* isBlacklisted */, BinaryDictionary.NOT_A_VALID_TIMESTAMP); + if (!prevWordsInfo.isValid() && mUseFirstLastBigrams) { + runGCIfRequiredLocked(true /* mindsBlockByGC */); + addNgramEntryLocked(prevWordsInfo, word, FREQUENCY_FOR_CONTACTS_BIGRAM, + BinaryDictionary.NOT_A_VALID_TIMESTAMP); } - prevWord = word; + prevWordsInfo = prevWordsInfo.getNextPrevWordsInfo( + new PrevWordsInfo.WordInfo(word)); } } } @@ -243,13 +268,7 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { return end; } - @Override - protected boolean needsToReloadBeforeWriting() { - return true; - } - - @Override - protected boolean hasContentChanged() { + private boolean haveContentsChanged() { final long startTime = SystemClock.uptimeMillis(); final int contactCount = getContactCount(); if (contactCount > MAX_CONTACT_COUNT) { @@ -258,9 +277,9 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { // TODO: Sort and check only the MAX_CONTACT_COUNT most recent contacts? return false; } - if (contactCount != sContactCountAtLastRebuild) { + if (contactCount != mContactCountAtLastRebuild) { if (DEBUG) { - Log.d(TAG, "Contact count changed: " + sContactCountAtLastRebuild + " to " + Log.d(TAG, "Contact count changed: " + mContactCountAtLastRebuild + " to " + contactCount); } return true; @@ -268,26 +287,27 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { // Check all contacts since it's not possible to find out which names have changed. // This is needed because it's possible to receive extraneous onChange events even when no // name has changed. - Cursor cursor = mContext.getContentResolver().query( - Contacts.CONTENT_URI, PROJECTION, null, null, null); - if (cursor != null) { - try { - if (cursor.moveToFirst()) { - while (!cursor.isAfterLast()) { - String name = cursor.getString(INDEX_NAME); - if (isValidName(name) && !isNameInDictionary(name)) { - if (DEBUG) { - Log.d(TAG, "Contact name missing: " + name + " (runtime = " - + (SystemClock.uptimeMillis() - startTime) + " ms)"); - } - return true; - } - cursor.moveToNext(); + final Cursor cursor = mContext.getContentResolver().query(Contacts.CONTENT_URI, PROJECTION, + null, null, null); + if (null == cursor) { + return false; + } + final ArrayList<String> names = new ArrayList<>(); + try { + if (cursor.moveToFirst()) { + while (!cursor.isAfterLast()) { + String name = cursor.getString(INDEX_NAME); + if (isValidName(name)) { + names.add(name); } + cursor.moveToNext(); } - } finally { - cursor.close(); } + if (names.hashCode() != mHashCodeAtLastRebuild) { + return true; + } + } finally { + cursor.close(); } if (DEBUG) { Log.d(TAG, "No contacts changed. (runtime = " + (SystemClock.uptimeMillis() - startTime) @@ -302,33 +322,4 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { } return false; } - - /** - * Checks if the words in a name are in the current binary dictionary. - */ - private boolean isNameInDictionary(final String name) { - 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 = StringUtils.codePointCount(word); - if (wordLen < MAX_WORD_LENGTH && wordLen > 1) { - if (!TextUtils.isEmpty(prevWord) && mUseFirstLastBigrams) { - if (!super.isValidBigramLocked(prevWord, word)) { - return false; - } - } else { - if (!super.isValidWordLocked(word)) { - return false; - } - } - prevWord = word; - } - } - } - return true; - } } diff --git a/java/src/com/android/inputmethod/latin/DicTraverseSession.java b/java/src/com/android/inputmethod/latin/DicTraverseSession.java index 8d295adee..b341f623e 100644 --- a/java/src/com/android/inputmethod/latin/DicTraverseSession.java +++ b/java/src/com/android/inputmethod/latin/DicTraverseSession.java @@ -16,6 +16,7 @@ package com.android.inputmethod.latin; +import com.android.inputmethod.latin.settings.NativeSuggestOptions; import com.android.inputmethod.latin.utils.JniUtils; import java.util.Locale; @@ -24,6 +25,24 @@ public final class DicTraverseSession { static { JniUtils.loadNativeLibrary(); } + // Must be equal to MAX_RESULTS in native/jni/src/defines.h + private static final int MAX_RESULTS = 18; + public final int[] mInputCodePoints = new int[Constants.DICTIONARY_MAX_WORD_LENGTH]; + public final int[][] mPrevWordCodePointArrays = + new int[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM][]; + public final boolean[] mIsBeginningOfSentenceArray = + new boolean[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM]; + public final int[] mOutputSuggestionCount = new int[1]; + public final int[] mOutputCodePoints = + new int[Constants.DICTIONARY_MAX_WORD_LENGTH * MAX_RESULTS]; + public final int[] mSpaceIndices = new int[MAX_RESULTS]; + public final int[] mOutputScores = new int[MAX_RESULTS]; + public final int[] mOutputTypes = new int[MAX_RESULTS]; + // Only one result is ever used + public final int[] mOutputAutoCommitFirstWordConfidence = new int[1]; + public final float[] mInputOutputLanguageWeight = new float[1]; + + public final NativeSuggestOptions mNativeSuggestOptions = new NativeSuggestOptions(); private static native long setDicTraverseSessionNative(String locale, long dictSize); private static native void initDicTraverseSessionNative(long nativeDicTraverseSession, diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java index fa79f5af7..b55ed125f 100644 --- a/java/src/com/android/inputmethod/latin/Dictionary.java +++ b/java/src/com/android/inputmethod/latin/Dictionary.java @@ -16,6 +16,7 @@ package com.android.inputmethod.latin; +import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; @@ -27,6 +28,7 @@ import java.util.ArrayList; */ public abstract class Dictionary { public static final int NOT_A_PROBABILITY = -1; + public static final float NOT_A_LANGUAGE_WEIGHT = -1.0f; // The following types do not actually come from real dictionary instances, so we create // corresponding instances. @@ -52,13 +54,12 @@ public abstract class Dictionary { public static final String TYPE_CONTACTS = "contacts"; // User dictionary, the system-managed one. public static final String TYPE_USER = "user"; - // User history dictionary internal to LatinIME. This assumes bigram prediction for now. + // User history dictionary internal to LatinIME. public static final String TYPE_USER_HISTORY = "history"; - // Personalization binary dictionary internal to LatinIME. + // Personalization dictionary. public static final String TYPE_PERSONALIZATION = "personalization"; - // Personalization prediction dictionary internal to LatinIME's Java code. - public static final String TYPE_PERSONALIZATION_PREDICTION_IN_JAVA = - "personalization_prediction_in_java"; + // Contextual dictionary. + public static final String TYPE_CONTEXTUAL = "contextual"; public final String mDictType; public Dictionary(final String dictType) { @@ -69,39 +70,44 @@ public abstract class Dictionary { * 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 prevWordsInfo the information of previous words. * @param proximityInfo the object for key proximity. May be ignored by some implementations. * @param blockOffensiveWords whether to block potentially offensive words * @param additionalFeaturesOptions options about additional features used for the suggestion. + * @param sessionId the session id. + * @param inOutLanguageWeight the language weight used for generating suggestions. + * inOutLanguageWeight is a float array that has only one element. This can be updated when the + * different language weight is used. * @return the list of suggestions (possibly null if none) */ - // TODO: pass more context than just the previous word, to enable better suggestions (n-gram - // and more) abstract public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, - final String prevWord, final ProximityInfo proximityInfo, - final boolean blockOffensiveWords, final int[] additionalFeaturesOptions); - - // The default implementation of this method ignores sessionId. - // Subclasses that want to use sessionId need to override this method. - public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer, - final String prevWord, final ProximityInfo proximityInfo, + final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, - final int sessionId) { - return getSuggestions(composer, prevWord, proximityInfo, blockOffensiveWords, - additionalFeaturesOptions); - } + final int sessionId, final float[] inOutLanguageWeight); /** - * Checks if the given word occurs in the dictionary + * Checks if the given word has to be treated as a valid word. Please note that some + * dictionaries have entries that should be treated as invalid words. * @param word the word to search for. The search should be case-insensitive. - * @return true if the word exists, false otherwise + * @return true if the word is valid, false otherwise */ - abstract public boolean isValidWord(final String word); + public boolean isValidWord(final String word) { + return isInDictionary(word); + } + + /** + * Checks if the given word is in the dictionary regardless of it being valid or not. + */ + abstract public boolean isInDictionary(final String word); public int getFrequency(final String word) { return NOT_A_PROBABILITY; } + public int getMaxFrequencyOfExactMatches(final String word) { + return NOT_A_PROBABILITY; + } + /** * Compares the contents of the character array with the typed word and returns true if they * are the same. @@ -161,13 +167,14 @@ public abstract class Dictionary { @Override public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, - final String prevWord, final ProximityInfo proximityInfo, - final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) { + final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, + final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, + final int sessionId, final float[] inOutLanguageWeight) { return null; } @Override - public boolean isValidWord(String word) { + public boolean isInDictionary(String word) { return false; } } diff --git a/java/src/com/android/inputmethod/latin/DictionaryCollection.java b/java/src/com/android/inputmethod/latin/DictionaryCollection.java index bf075140e..89d61ce2a 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryCollection.java +++ b/java/src/com/android/inputmethod/latin/DictionaryCollection.java @@ -20,7 +20,6 @@ import android.util.Log; import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; -import com.android.inputmethod.latin.utils.CollectionUtils; import java.util.ArrayList; import java.util.Collection; @@ -36,49 +35,52 @@ public final class DictionaryCollection extends Dictionary { public DictionaryCollection(final String dictType) { super(dictType); - mDictionaries = CollectionUtils.newCopyOnWriteArrayList(); + mDictionaries = new CopyOnWriteArrayList<>(); } public DictionaryCollection(final String dictType, final Dictionary... dictionaries) { super(dictType); if (null == dictionaries) { - mDictionaries = CollectionUtils.newCopyOnWriteArrayList(); + mDictionaries = new CopyOnWriteArrayList<>(); } else { - mDictionaries = CollectionUtils.newCopyOnWriteArrayList(dictionaries); + mDictionaries = new CopyOnWriteArrayList<>(dictionaries); mDictionaries.removeAll(Collections.singleton(null)); } } public DictionaryCollection(final String dictType, final Collection<Dictionary> dictionaries) { super(dictType); - mDictionaries = CollectionUtils.newCopyOnWriteArrayList(dictionaries); + mDictionaries = new CopyOnWriteArrayList<>(dictionaries); mDictionaries.removeAll(Collections.singleton(null)); } @Override public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, - final String prevWord, final ProximityInfo proximityInfo, - final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) { + final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, + final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, + final int sessionId, final float[] inOutLanguageWeight) { final CopyOnWriteArrayList<Dictionary> dictionaries = mDictionaries; if (dictionaries.isEmpty()) return null; // To avoid creating unnecessary objects, we get the list out of the first // dictionary and add the rest to it if not null, hence the get(0) ArrayList<SuggestedWordInfo> suggestions = dictionaries.get(0).getSuggestions(composer, - prevWord, proximityInfo, blockOffensiveWords, additionalFeaturesOptions); - if (null == suggestions) suggestions = CollectionUtils.newArrayList(); + prevWordsInfo, proximityInfo, blockOffensiveWords, additionalFeaturesOptions, + sessionId, inOutLanguageWeight); + if (null == suggestions) suggestions = new ArrayList<>(); final int length = dictionaries.size(); for (int i = 1; i < length; ++ i) { final ArrayList<SuggestedWordInfo> sugg = dictionaries.get(i).getSuggestions(composer, - prevWord, proximityInfo, blockOffensiveWords, additionalFeaturesOptions); + prevWordsInfo, proximityInfo, blockOffensiveWords, additionalFeaturesOptions, + sessionId, inOutLanguageWeight); if (null != sugg) suggestions.addAll(sugg); } return suggestions; } @Override - public boolean isValidWord(final String word) { + public boolean isInDictionary(final String word) { for (int i = mDictionaries.size() - 1; i >= 0; --i) - if (mDictionaries.get(i).isValidWord(word)) return true; + if (mDictionaries.get(i).isInDictionary(word)) return true; return false; } @@ -87,9 +89,17 @@ public final class DictionaryCollection extends Dictionary { int maxFreq = -1; for (int i = mDictionaries.size() - 1; i >= 0; --i) { final int tempFreq = mDictionaries.get(i).getFrequency(word); - if (tempFreq >= maxFreq) { - maxFreq = tempFreq; - } + maxFreq = Math.max(tempFreq, maxFreq); + } + return maxFreq; + } + + @Override + public int getMaxFrequencyOfExactMatches(final String word) { + int maxFreq = -1; + for (int i = mDictionaries.size() - 1; i >= 0; --i) { + final int tempFreq = mDictionaries.get(i).getMaxFrequencyOfExactMatches(word); + maxFreq = Math.max(tempFreq, maxFreq); } return maxFreq; } diff --git a/java/src/com/android/inputmethod/latin/DictionaryDumpBroadcastReceiver.java b/java/src/com/android/inputmethod/latin/DictionaryDumpBroadcastReceiver.java new file mode 100644 index 000000000..ee2fdc6c7 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/DictionaryDumpBroadcastReceiver.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +public class DictionaryDumpBroadcastReceiver extends BroadcastReceiver { + private static final String TAG = DictionaryDumpBroadcastReceiver.class.getSimpleName(); + + private static final String DOMAIN = "com.android.inputmethod.latin"; + public static final String DICTIONARY_DUMP_INTENT_ACTION = DOMAIN + ".DICT_DUMP"; + public static final String DICTIONARY_NAME_KEY = "dictName"; + + final LatinIME mLatinIme; + + public DictionaryDumpBroadcastReceiver(final LatinIME latinIme) { + mLatinIme = latinIme; + } + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (action.equals(DICTIONARY_DUMP_INTENT_ACTION)) { + final String dictName = intent.getStringExtra(DICTIONARY_NAME_KEY); + if (dictName == null) { + Log.e(TAG, "Received dictionary dump intent action " + + "but the dictionary name is not set."); + return; + } + mLatinIme.dumpDictionaryForDebug(dictName); + } + } +} diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java new file mode 100644 index 000000000..b8feb2278 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java @@ -0,0 +1,658 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin; + +import android.content.Context; +import android.text.TextUtils; +import android.util.Log; +import android.view.inputmethod.InputMethodSubtype; + +import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.keyboard.ProximityInfo; +import com.android.inputmethod.latin.PrevWordsInfo.WordInfo; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import com.android.inputmethod.latin.personalization.ContextualDictionary; +import com.android.inputmethod.latin.personalization.PersonalizationDataChunk; +import com.android.inputmethod.latin.personalization.PersonalizationDictionary; +import com.android.inputmethod.latin.personalization.UserHistoryDictionary; +import com.android.inputmethod.latin.settings.SpacingAndPunctuations; +import com.android.inputmethod.latin.utils.DistracterFilter; +import com.android.inputmethod.latin.utils.DistracterFilterCheckingIsInDictionary; +import com.android.inputmethod.latin.utils.ExecutorUtils; +import com.android.inputmethod.latin.utils.LanguageModelParam; +import com.android.inputmethod.latin.utils.SuggestionResults; + +import java.io.File; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +// TODO: Consolidate dictionaries in native code. +public class DictionaryFacilitator { + public static final String TAG = DictionaryFacilitator.class.getSimpleName(); + + // HACK: This threshold is being used when adding a capitalized entry in the User History + // dictionary. + private static final int CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT = 140; + + private Dictionaries mDictionaries = new Dictionaries(); + private boolean mIsUserDictEnabled = false; + private volatile CountDownLatch mLatchForWaitingLoadingMainDictionary = new CountDownLatch(0); + // To synchronize assigning mDictionaries to ensure closing dictionaries. + private final Object mLock = new Object(); + private final DistracterFilter mDistracterFilter; + + private static final String[] DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS = + new String[] { + Dictionary.TYPE_MAIN, + Dictionary.TYPE_USER_HISTORY, + Dictionary.TYPE_PERSONALIZATION, + Dictionary.TYPE_USER, + Dictionary.TYPE_CONTACTS, + Dictionary.TYPE_CONTEXTUAL + }; + + public static final Map<String, Class<? extends ExpandableBinaryDictionary>> + DICT_TYPE_TO_CLASS = new HashMap<>(); + + static { + DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_USER_HISTORY, UserHistoryDictionary.class); + DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_PERSONALIZATION, PersonalizationDictionary.class); + DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_USER, UserBinaryDictionary.class); + DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_CONTACTS, ContactsBinaryDictionary.class); + DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_CONTEXTUAL, ContextualDictionary.class); + } + + private static final String DICT_FACTORY_METHOD_NAME = "getDictionary"; + private static final Class<?>[] DICT_FACTORY_METHOD_ARG_TYPES = + new Class[] { Context.class, Locale.class, File.class, String.class }; + + private static final String[] SUB_DICT_TYPES = + Arrays.copyOfRange(DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS, 1 /* start */, + DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS.length); + + /** + * Class contains dictionaries for a locale. + */ + private static class Dictionaries { + public final Locale mLocale; + private Dictionary mMainDict; + public final ConcurrentHashMap<String, ExpandableBinaryDictionary> mSubDictMap = + new ConcurrentHashMap<>(); + + public Dictionaries() { + mLocale = null; + } + + public Dictionaries(final Locale locale, final Dictionary mainDict, + final Map<String, ExpandableBinaryDictionary> subDicts) { + mLocale = locale; + // Main dictionary can be asynchronously loaded. + setMainDict(mainDict); + for (final Map.Entry<String, ExpandableBinaryDictionary> entry : subDicts.entrySet()) { + setSubDict(entry.getKey(), entry.getValue()); + } + } + + private void setSubDict(final String dictType, final ExpandableBinaryDictionary dict) { + if (dict != null) { + mSubDictMap.put(dictType, dict); + } + } + + public void setMainDict(final Dictionary mainDict) { + // Close old dictionary if exists. Main dictionary can be assigned multiple times. + final Dictionary oldDict = mMainDict; + mMainDict = mainDict; + if (oldDict != null && mainDict != oldDict) { + oldDict.close(); + } + } + + public Dictionary getDict(final String dictType) { + if (Dictionary.TYPE_MAIN.equals(dictType)) { + return mMainDict; + } else { + return getSubDict(dictType); + } + } + + public ExpandableBinaryDictionary getSubDict(final String dictType) { + return mSubDictMap.get(dictType); + } + + public boolean hasDict(final String dictType) { + if (Dictionary.TYPE_MAIN.equals(dictType)) { + return mMainDict != null; + } else { + return mSubDictMap.containsKey(dictType); + } + } + + public void closeDict(final String dictType) { + final Dictionary dict; + if (Dictionary.TYPE_MAIN.equals(dictType)) { + dict = mMainDict; + } else { + dict = mSubDictMap.remove(dictType); + } + if (dict != null) { + dict.close(); + } + } + } + + public interface DictionaryInitializationListener { + public void onUpdateMainDictionaryAvailability(boolean isMainDictionaryAvailable); + } + + public DictionaryFacilitator() { + mDistracterFilter = DistracterFilter.EMPTY_DISTRACTER_FILTER; + } + + public DictionaryFacilitator(final DistracterFilter distracterFilter) { + mDistracterFilter = distracterFilter; + } + + public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes) { + mDistracterFilter.updateEnabledSubtypes(enabledSubtypes); + } + + public Locale getLocale() { + return mDictionaries.mLocale; + } + + private static ExpandableBinaryDictionary getSubDict(final String dictType, + final Context context, final Locale locale, final File dictFile, + final String dictNamePrefix) { + final Class<? extends ExpandableBinaryDictionary> dictClass = + DICT_TYPE_TO_CLASS.get(dictType); + if (dictClass == null) { + return null; + } + try { + final Method factoryMethod = dictClass.getMethod(DICT_FACTORY_METHOD_NAME, + DICT_FACTORY_METHOD_ARG_TYPES); + final Object dict = factoryMethod.invoke(null /* obj */, + new Object[] { context, locale, dictFile, dictNamePrefix }); + return (ExpandableBinaryDictionary) dict; + } catch (final NoSuchMethodException | SecurityException | IllegalAccessException + | IllegalArgumentException | InvocationTargetException e) { + Log.e(TAG, "Cannot create dictionary: " + dictType, e); + return null; + } + } + + public void resetDictionaries(final Context context, final Locale newLocale, + final boolean useContactsDict, final boolean usePersonalizedDicts, + final boolean forceReloadMainDictionary, + final DictionaryInitializationListener listener) { + resetDictionariesWithDictNamePrefix(context, newLocale, useContactsDict, + usePersonalizedDicts, forceReloadMainDictionary, listener, "" /* dictNamePrefix */); + } + + public void resetDictionariesWithDictNamePrefix(final Context context, final Locale newLocale, + final boolean useContactsDict, final boolean usePersonalizedDicts, + final boolean forceReloadMainDictionary, + final DictionaryInitializationListener listener, + final String dictNamePrefix) { + final boolean localeHasBeenChanged = !newLocale.equals(mDictionaries.mLocale); + // We always try to have the main dictionary. Other dictionaries can be unused. + final boolean reloadMainDictionary = localeHasBeenChanged || forceReloadMainDictionary; + // TODO: Make subDictTypesToUse configurable by resource or a static final list. + final HashSet<String> subDictTypesToUse = new HashSet<>(); + if (useContactsDict) { + subDictTypesToUse.add(Dictionary.TYPE_CONTACTS); + } + subDictTypesToUse.add(Dictionary.TYPE_USER); + if (usePersonalizedDicts) { + subDictTypesToUse.add(Dictionary.TYPE_USER_HISTORY); + subDictTypesToUse.add(Dictionary.TYPE_PERSONALIZATION); + subDictTypesToUse.add(Dictionary.TYPE_CONTEXTUAL); + } + + final Dictionary newMainDict; + if (reloadMainDictionary) { + // The main dictionary will be asynchronously loaded. + newMainDict = null; + } else { + newMainDict = mDictionaries.getDict(Dictionary.TYPE_MAIN); + } + + final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>(); + for (final String dictType : SUB_DICT_TYPES) { + if (!subDictTypesToUse.contains(dictType)) { + // This dictionary will not be used. + continue; + } + final ExpandableBinaryDictionary dict; + if (!localeHasBeenChanged && mDictionaries.hasDict(dictType)) { + // Continue to use current dictionary. + dict = mDictionaries.getSubDict(dictType); + } else { + // Start to use new dictionary. + dict = getSubDict(dictType, context, newLocale, null /* dictFile */, + dictNamePrefix); + } + subDicts.put(dictType, dict); + } + + // Replace Dictionaries. + final Dictionaries newDictionaries = new Dictionaries(newLocale, newMainDict, subDicts); + final Dictionaries oldDictionaries; + synchronized (mLock) { + oldDictionaries = mDictionaries; + mDictionaries = newDictionaries; + mIsUserDictEnabled = UserBinaryDictionary.isEnabled(context); + if (reloadMainDictionary) { + asyncReloadMainDictionary(context, newLocale, listener); + } + } + if (listener != null) { + listener.onUpdateMainDictionaryAvailability(hasInitializedMainDictionary()); + } + // Clean up old dictionaries. + if (reloadMainDictionary) { + oldDictionaries.closeDict(Dictionary.TYPE_MAIN); + } + for (final String dictType : SUB_DICT_TYPES) { + if (localeHasBeenChanged || !subDictTypesToUse.contains(dictType)) { + oldDictionaries.closeDict(dictType); + } + } + oldDictionaries.mSubDictMap.clear(); + } + + private void asyncReloadMainDictionary(final Context context, final Locale locale, + final DictionaryInitializationListener listener) { + final CountDownLatch latchForWaitingLoadingMainDictionary = new CountDownLatch(1); + mLatchForWaitingLoadingMainDictionary = latchForWaitingLoadingMainDictionary; + ExecutorUtils.getExecutor("InitializeBinaryDictionary").execute(new Runnable() { + @Override + public void run() { + final Dictionary mainDict = + DictionaryFactory.createMainDictionaryFromManager(context, locale); + synchronized (mLock) { + if (locale.equals(mDictionaries.mLocale)) { + mDictionaries.setMainDict(mainDict); + } else { + // Dictionary facilitator has been reset for another locale. + mainDict.close(); + } + } + if (listener != null) { + listener.onUpdateMainDictionaryAvailability(hasInitializedMainDictionary()); + } + latchForWaitingLoadingMainDictionary.countDown(); + } + }); + } + + @UsedForTesting + public void resetDictionariesForTesting(final Context context, final Locale locale, + final ArrayList<String> dictionaryTypes, final HashMap<String, File> dictionaryFiles, + final Map<String, Map<String, String>> additionalDictAttributes) { + Dictionary mainDictionary = null; + final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>(); + + for (final String dictType : dictionaryTypes) { + if (dictType.equals(Dictionary.TYPE_MAIN)) { + mainDictionary = DictionaryFactory.createMainDictionaryFromManager(context, locale); + } else { + final File dictFile = dictionaryFiles.get(dictType); + final ExpandableBinaryDictionary dict = getSubDict( + dictType, context, locale, dictFile, "" /* dictNamePrefix */); + if (additionalDictAttributes.containsKey(dictType)) { + dict.clearAndFlushDictionaryWithAdditionalAttributes( + additionalDictAttributes.get(dictType)); + } + if (dict == null) { + throw new RuntimeException("Unknown dictionary type: " + dictType); + } + dict.reloadDictionaryIfRequired(); + dict.waitAllTasksForTests(); + subDicts.put(dictType, dict); + } + } + mDictionaries = new Dictionaries(locale, mainDictionary, subDicts); + } + + public void closeDictionaries() { + final Dictionaries dictionaries; + synchronized (mLock) { + dictionaries = mDictionaries; + mDictionaries = new Dictionaries(); + } + for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { + dictionaries.closeDict(dictType); + } + mDistracterFilter.close(); + } + + @UsedForTesting + public ExpandableBinaryDictionary getSubDictForTesting(final String dictName) { + return mDictionaries.getSubDict(dictName); + } + + // The main dictionary could have been loaded asynchronously. Don't cache the return value + // of this method. + public boolean hasInitializedMainDictionary() { + final Dictionary mainDict = mDictionaries.getDict(Dictionary.TYPE_MAIN); + return mainDict != null && mainDict.isInitialized(); + } + + public boolean hasPersonalizationDictionary() { + return mDictionaries.hasDict(Dictionary.TYPE_PERSONALIZATION); + } + + public void flushPersonalizationDictionary() { + final ExpandableBinaryDictionary personalizationDict = + mDictionaries.getSubDict(Dictionary.TYPE_PERSONALIZATION); + if (personalizationDict != null) { + personalizationDict.asyncFlushBinaryDictionary(); + } + } + + public void waitForLoadingMainDictionary(final long timeout, final TimeUnit unit) + throws InterruptedException { + mLatchForWaitingLoadingMainDictionary.await(timeout, unit); + } + + @UsedForTesting + public void waitForLoadingDictionariesForTesting(final long timeout, final TimeUnit unit) + throws InterruptedException { + waitForLoadingMainDictionary(timeout, unit); + final Map<String, ExpandableBinaryDictionary> dictMap = mDictionaries.mSubDictMap; + for (final ExpandableBinaryDictionary dict : dictMap.values()) { + dict.waitAllTasksForTests(); + } + } + + public boolean isUserDictionaryEnabled() { + return mIsUserDictEnabled; + } + + public void addWordToUserDictionary(final Context context, final String word) { + final Locale locale = getLocale(); + if (locale == null) { + return; + } + UserBinaryDictionary.addWordToUserDictionary(context, locale, word); + } + + public void addToUserHistory(final String suggestion, final boolean wasAutoCapitalized, + final PrevWordsInfo prevWordsInfo, final int timeStampInSeconds, + final boolean blockPotentiallyOffensive) { + final Dictionaries dictionaries = mDictionaries; + final String[] words = suggestion.split(Constants.WORD_SEPARATOR); + PrevWordsInfo prevWordsInfoForCurrentWord = prevWordsInfo; + for (int i = 0; i < words.length; i++) { + final String currentWord = words[i]; + final boolean wasCurrentWordAutoCapitalized = (i == 0) ? wasAutoCapitalized : false; + addWordToUserHistory(dictionaries, prevWordsInfoForCurrentWord, currentWord, + wasCurrentWordAutoCapitalized, timeStampInSeconds, blockPotentiallyOffensive); + prevWordsInfoForCurrentWord = + prevWordsInfoForCurrentWord.getNextPrevWordsInfo(new WordInfo(currentWord)); + } + } + + private void addWordToUserHistory(final Dictionaries dictionaries, + final PrevWordsInfo prevWordsInfo, final String word, final boolean wasAutoCapitalized, + final int timeStampInSeconds, final boolean blockPotentiallyOffensive) { + final ExpandableBinaryDictionary userHistoryDictionary = + dictionaries.getSubDict(Dictionary.TYPE_USER_HISTORY); + if (userHistoryDictionary == null) { + return; + } + final int maxFreq = getFrequency(word); + if (maxFreq == 0 && blockPotentiallyOffensive) { + return; + } + final String lowerCasedWord = word.toLowerCase(dictionaries.mLocale); + final String secondWord; + if (wasAutoCapitalized) { + if (isValidWord(word, false /* ignoreCase */) + && !isValidWord(lowerCasedWord, false /* ignoreCase */)) { + // If the word was auto-capitalized and exists only as a capitalized word in the + // dictionary, then we must not downcase it before registering it. For example, + // the name of the contacts in start-of-sentence position would come here with the + // wasAutoCapitalized flag: if we downcase it, we'd register a lower-case version + // of that contact's name which would end up popping in suggestions. + secondWord = word; + } else { + // If however the word is not in the dictionary, or exists as a lower-case word + // only, then we consider that was a lower-case word that had been auto-capitalized. + secondWord = lowerCasedWord; + } + } else { + // HACK: We'd like to avoid adding the capitalized form of common words to the User + // History dictionary in order to avoid suggesting them until the dictionary + // consolidation is done. + // TODO: Remove this hack when ready. + final int lowerCaseFreqInMainDict = dictionaries.hasDict(Dictionary.TYPE_MAIN) ? + dictionaries.getDict(Dictionary.TYPE_MAIN).getFrequency(lowerCasedWord) : + Dictionary.NOT_A_PROBABILITY; + if (maxFreq < lowerCaseFreqInMainDict + && lowerCaseFreqInMainDict >= CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT) { + // Use lower cased word as the word can be a distracter of the popular word. + secondWord = lowerCasedWord; + } else { + secondWord = word; + } + } + // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid". + // We don't add words with 0-frequency (assuming they would be profanity etc.). + final boolean isValid = maxFreq > 0; + UserHistoryDictionary.addToDictionary(userHistoryDictionary, prevWordsInfo, secondWord, + isValid, timeStampInSeconds, mDistracterFilter); + } + + private void removeWord(final String dictName, final String word) { + final ExpandableBinaryDictionary dictionary = mDictionaries.getSubDict(dictName); + if (dictionary != null) { + dictionary.removeUnigramEntryDynamically(word); + } + } + + public void removeWordFromPersonalizedDicts(final String word) { + removeWord(Dictionary.TYPE_USER_HISTORY, word); + removeWord(Dictionary.TYPE_PERSONALIZATION, word); + removeWord(Dictionary.TYPE_CONTEXTUAL, word); + } + + // TODO: Revise the way to fusion suggestion results. + public SuggestionResults getSuggestionResults(final WordComposer composer, + final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, + final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, + final int sessionId) { + final Dictionaries dictionaries = mDictionaries; + final SuggestionResults suggestionResults = + new SuggestionResults(dictionaries.mLocale, SuggestedWords.MAX_SUGGESTIONS); + final float[] languageWeight = new float[] { Dictionary.NOT_A_LANGUAGE_WEIGHT }; + for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { + final Dictionary dictionary = dictionaries.getDict(dictType); + if (null == dictionary) continue; + final ArrayList<SuggestedWordInfo> dictionarySuggestions = + dictionary.getSuggestions(composer, prevWordsInfo, proximityInfo, + blockOffensiveWords, additionalFeaturesOptions, sessionId, + languageWeight); + if (null == dictionarySuggestions) continue; + suggestionResults.addAll(dictionarySuggestions); + if (null != suggestionResults.mRawSuggestions) { + suggestionResults.mRawSuggestions.addAll(dictionarySuggestions); + } + } + return suggestionResults; + } + + public boolean isValidWord(final String word, final boolean ignoreCase) { + if (TextUtils.isEmpty(word)) { + return false; + } + final Dictionaries dictionaries = mDictionaries; + if (dictionaries.mLocale == null) { + return false; + } + final String lowerCasedWord = word.toLowerCase(dictionaries.mLocale); + for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { + final Dictionary dictionary = dictionaries.getDict(dictType); + // Ideally the passed map would come out of a {@link java.util.concurrent.Future} and + // would be immutable once it's finished initializing, but concretely a null test is + // probably good enough for the time being. + if (null == dictionary) continue; + if (dictionary.isValidWord(word) + || (ignoreCase && dictionary.isValidWord(lowerCasedWord))) { + return true; + } + } + return false; + } + + private int getFrequencyInternal(final String word, + final boolean isGettingMaxFrequencyOfExactMatches) { + if (TextUtils.isEmpty(word)) { + return Dictionary.NOT_A_PROBABILITY; + } + int maxFreq = Dictionary.NOT_A_PROBABILITY; + final Dictionaries dictionaries = mDictionaries; + for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { + final Dictionary dictionary = dictionaries.getDict(dictType); + if (dictionary == null) continue; + final int tempFreq; + if (isGettingMaxFrequencyOfExactMatches) { + tempFreq = dictionary.getMaxFrequencyOfExactMatches(word); + } else { + tempFreq = dictionary.getFrequency(word); + } + if (tempFreq >= maxFreq) { + maxFreq = tempFreq; + } + } + return maxFreq; + } + + public int getFrequency(final String word) { + return getFrequencyInternal(word, false /* isGettingMaxFrequencyOfExactMatches */); + } + + public int getMaxFrequencyOfExactMatches(final String word) { + return getFrequencyInternal(word, true /* isGettingMaxFrequencyOfExactMatches */); + } + + private void clearSubDictionary(final String dictName) { + final ExpandableBinaryDictionary dictionary = mDictionaries.getSubDict(dictName); + if (dictionary != null) { + dictionary.clear(); + } + } + + public void clearUserHistoryDictionary() { + clearSubDictionary(Dictionary.TYPE_USER_HISTORY); + } + + // This method gets called only when the IME receives a notification to remove the + // personalization dictionary. + public void clearPersonalizationDictionary() { + clearSubDictionary(Dictionary.TYPE_PERSONALIZATION); + } + + public void clearContextualDictionary() { + clearSubDictionary(Dictionary.TYPE_CONTEXTUAL); + } + + public void addEntriesToPersonalizationDictionary( + final PersonalizationDataChunk personalizationDataChunk, + final SpacingAndPunctuations spacingAndPunctuations, + final ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback callback) { + final ExpandableBinaryDictionary personalizationDict = + mDictionaries.getSubDict(Dictionary.TYPE_PERSONALIZATION); + if (personalizationDict == null) { + if (callback != null) { + callback.onFinished(); + } + return; + } + final ArrayList<LanguageModelParam> languageModelParams = + LanguageModelParam.createLanguageModelParamsFrom( + personalizationDataChunk.mTokens, + personalizationDataChunk.mTimestampInSeconds, + this /* dictionaryFacilitator */, spacingAndPunctuations, + new DistracterFilterCheckingIsInDictionary( + mDistracterFilter, personalizationDict)); + if (languageModelParams == null || languageModelParams.isEmpty()) { + if (callback != null) { + callback.onFinished(); + } + return; + } + personalizationDict.addMultipleDictionaryEntriesDynamically(languageModelParams, callback); + } + + public void addPhraseToContextualDictionary(final String[] phrase, final int probability, + final int bigramProbabilityForWords, final int bigramProbabilityForPhrases) { + final ExpandableBinaryDictionary contextualDict = + mDictionaries.getSubDict(Dictionary.TYPE_CONTEXTUAL); + if (contextualDict == null) { + return; + } + PrevWordsInfo prevWordsInfo = PrevWordsInfo.BEGINNING_OF_SENTENCE; + for (int i = 0; i < phrase.length; i++) { + final String[] subPhrase = Arrays.copyOfRange(phrase, i /* start */, phrase.length); + final String subPhraseStr = TextUtils.join(Constants.WORD_SEPARATOR, subPhrase); + contextualDict.addUnigramEntryWithCheckingDistracter( + subPhraseStr, probability, null /* shortcutTarget */, + Dictionary.NOT_A_PROBABILITY /* shortcutFreq */, + false /* isNotAWord */, false /* isBlacklisted */, + BinaryDictionary.NOT_A_VALID_TIMESTAMP, + DistracterFilter.EMPTY_DISTRACTER_FILTER); + contextualDict.addNgramEntry(prevWordsInfo, subPhraseStr, + bigramProbabilityForPhrases, BinaryDictionary.NOT_A_VALID_TIMESTAMP); + + if (i < phrase.length - 1) { + contextualDict.addUnigramEntryWithCheckingDistracter( + phrase[i], probability, null /* shortcutTarget */, + Dictionary.NOT_A_PROBABILITY /* shortcutFreq */, + false /* isNotAWord */, false /* isBlacklisted */, + BinaryDictionary.NOT_A_VALID_TIMESTAMP, + DistracterFilter.EMPTY_DISTRACTER_FILTER); + contextualDict.addNgramEntry(prevWordsInfo, phrase[i], + bigramProbabilityForWords, BinaryDictionary.NOT_A_VALID_TIMESTAMP); + } + prevWordsInfo = + prevWordsInfo.getNextPrevWordsInfo(new PrevWordsInfo.WordInfo(phrase[i])); + } + } + + public void dumpDictionaryForDebug(final String dictName) { + final ExpandableBinaryDictionary dictToDump = mDictionaries.getSubDict(dictName); + if (dictToDump == null) { + Log.e(TAG, "Cannot dump " + dictName + ". " + + "The dictionary is not being used for suggestion or cannot be dumped."); + return; + } + dictToDump.dumpAllWordsForDebug(); + } +} diff --git a/java/src/com/android/inputmethod/latin/DictionaryFactory.java b/java/src/com/android/inputmethod/latin/DictionaryFactory.java index 828e54f14..59de4f82a 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFactory.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFactory.java @@ -16,13 +16,13 @@ package com.android.inputmethod.latin; +import android.content.ContentProviderClient; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.content.res.Resources; import android.util.Log; import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.DictionaryInfoUtils; import java.io.File; @@ -54,7 +54,7 @@ public final class DictionaryFactory { createReadOnlyBinaryDictionary(context, locale)); } - final LinkedList<Dictionary> dictList = CollectionUtils.newLinkedList(); + final LinkedList<Dictionary> dictList = new LinkedList<>(); final ArrayList<AssetFileAddress> assetFileList = BinaryDictionaryGetter.getDictionaryFiles(locale, context); if (null != assetFileList) { @@ -64,6 +64,10 @@ public final class DictionaryFactory { useFullEditDistance, locale, Dictionary.TYPE_MAIN); if (readOnlyBinaryDictionary.isValidDictionary()) { dictList.add(readOnlyBinaryDictionary); + } else { + readOnlyBinaryDictionary.close(); + // Prevent this dictionary to do any further harm. + killDictionary(context, f); } } } @@ -75,6 +79,51 @@ public final class DictionaryFactory { } /** + * Kills a dictionary so that it is never used again, if possible. + * @param context The context to contact the dictionary provider, if possible. + * @param f A file address to the dictionary to kill. + */ + private static void killDictionary(final Context context, final AssetFileAddress f) { + if (f.pointsToPhysicalFile()) { + f.deleteUnderlyingFile(); + // Warn the dictionary provider if the dictionary came from there. + final ContentProviderClient providerClient; + try { + providerClient = context.getContentResolver().acquireContentProviderClient( + BinaryDictionaryFileDumper.getProviderUriBuilder("").build()); + } catch (final SecurityException e) { + Log.e(TAG, "No permission to communicate with the dictionary provider", e); + return; + } + if (null == providerClient) { + Log.e(TAG, "Can't establish communication with the dictionary provider"); + return; + } + final String wordlistId = + DictionaryInfoUtils.getWordListIdFromFileName(new File(f.mFilename).getName()); + if (null != wordlistId) { + // TODO: this is a reasonable last resort, but it is suboptimal. + // The following will remove the entry for this dictionary with the dictionary + // provider. When the metadata is downloaded again, we will try downloading it + // again. + // However, in the practice that will mean the user will find themselves without + // the new dictionary. That's fine for languages where it's included in the APK, + // but for other languages it will leave the user without a dictionary at all until + // the next update, which may be a few days away. + // Ideally, we would trigger a new download right away, and use increasing retry + // delays for this particular id/version combination. + // Then again, this is expected to only ever happen in case of human mistake. If + // the wrong file is on the server, the following is still doing the right thing. + // If it's a file left over from the last version however, it's not great. + BinaryDictionaryFileDumper.reportBrokenFileToDictionaryProvider( + providerClient, + context.getString(R.string.dictionary_pack_client_id), + wordlistId); + } + } + } + + /** * Initializes a main dictionary collection from a dictionary pack, with default flags. * * This searches for a content provider providing a dictionary pack for the specified diff --git a/java/src/com/android/inputmethod/latin/DictionaryWriter.java b/java/src/com/android/inputmethod/latin/DictionaryWriter.java deleted file mode 100644 index 3df2a2b63..000000000 --- a/java/src/com/android/inputmethod/latin/DictionaryWriter.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin; - -import android.content.Context; - -import com.android.inputmethod.keyboard.ProximityInfo; -import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; -import com.android.inputmethod.latin.makedict.DictEncoder; -import com.android.inputmethod.latin.makedict.FormatSpec; -import com.android.inputmethod.latin.makedict.FusionDictionary; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; -import com.android.inputmethod.latin.makedict.UnsupportedFormatException; -import com.android.inputmethod.latin.utils.CollectionUtils; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; - -/** - * An in memory dictionary for memorizing entries and writing a binary dictionary. - */ -public class DictionaryWriter extends AbstractDictionaryWriter { - private static final int BINARY_DICT_VERSION = 3; - private static final FormatSpec.FormatOptions FORMAT_OPTIONS = - new FormatSpec.FormatOptions(BINARY_DICT_VERSION, true /* supportsDynamicUpdate */); - - private FusionDictionary mFusionDictionary; - - public DictionaryWriter(final Context context, final String dictType) { - super(context, dictType); - clear(); - } - - @Override - public void clear() { - final HashMap<String, String> attributes = CollectionUtils.newHashMap(); - mFusionDictionary = new FusionDictionary(new PtNodeArray(), - new FusionDictionary.DictionaryOptions(attributes, false, false)); - } - - /** - * Adds a word unigram to the fusion dictionary. - */ - // TODO: Create "cache dictionary" to cache fresh words for frequently updated dictionaries, - // considering performance regression. - @Override - public void addUnigramWord(final String word, final String shortcutTarget, final int frequency, - final int shortcutFreq, final boolean isNotAWord) { - if (shortcutTarget == null) { - mFusionDictionary.add(word, frequency, null, isNotAWord); - } else { - // TODO: Do this in the subclass, with this class taking an arraylist. - final ArrayList<WeightedString> shortcutTargets = CollectionUtils.newArrayList(); - shortcutTargets.add(new WeightedString(shortcutTarget, shortcutFreq)); - mFusionDictionary.add(word, frequency, shortcutTargets, isNotAWord); - } - } - - @Override - public void addBigramWords(final String word0, final String word1, final int frequency, - final boolean isValid, final long lastModifiedTime) { - mFusionDictionary.setBigram(word0, word1, frequency); - } - - @Override - public void removeBigramWords(final String word0, final String word1) { - // This class don't support removing bigram words. - } - - @Override - protected void writeDictionary(final DictEncoder dictEncoder, - final Map<String, String> attributeMap) throws IOException, UnsupportedFormatException { - for (final Map.Entry<String, String> entry : attributeMap.entrySet()) { - mFusionDictionary.addOptionAttribute(entry.getKey(), entry.getValue()); - } - dictEncoder.writeDictionary(mFusionDictionary, FORMAT_OPTIONS); - } - - @Override - public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, - final String prevWord, final ProximityInfo proximityInfo, - boolean blockOffensiveWords, final int[] additionalFeaturesOptions) { - // This class doesn't support suggestion. - return null; - } - - @Override - public boolean isValidWord(String word) { - // This class doesn't support dictionary retrieval. - return false; - } -} diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java index eb8650e6f..37879cf68 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java @@ -17,69 +17,57 @@ package com.android.inputmethod.latin; import android.content.Context; -import android.os.SystemClock; import android.util.Log; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.keyboard.ProximityInfo; +import com.android.inputmethod.latin.makedict.DictionaryHeader; import com.android.inputmethod.latin.makedict.FormatSpec; -import com.android.inputmethod.latin.personalization.DynamicPersonalizationDictionaryWriter; +import com.android.inputmethod.latin.makedict.UnsupportedFormatException; +import com.android.inputmethod.latin.makedict.WordProperty; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; -import com.android.inputmethod.latin.utils.AsyncResultHolder; -import com.android.inputmethod.latin.utils.CollectionUtils; -import com.android.inputmethod.latin.utils.PrioritizedSerialExecutor; +import com.android.inputmethod.latin.utils.CombinedFormatUtils; +import com.android.inputmethod.latin.utils.DistracterFilter; +import com.android.inputmethod.latin.utils.ExecutorUtils; +import com.android.inputmethod.latin.utils.FileUtils; +import com.android.inputmethod.latin.utils.LanguageModelParam; import java.io.File; import java.util.ArrayList; import java.util.HashMap; +import java.util.Locale; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; /** * Abstract base class for an expandable dictionary that can be created and updated dynamically * during runtime. When updated it automatically generates a new binary dictionary to handle future - * queries in native code. This binary dictionary is written to internal storage, and potentially - * shared across multiple ExpandableBinaryDictionary instances. Updates to each dictionary filename - * are controlled across multiple instances to ensure that only one instance can update the same - * dictionary at the same time. + * queries in native code. This binary dictionary is written to internal storage. */ abstract public class ExpandableBinaryDictionary extends Dictionary { + private static final boolean DEBUG = false; /** Used for Log actions from this class */ private static final String TAG = ExpandableBinaryDictionary.class.getSimpleName(); /** Whether to print debug output to log */ - private static boolean DEBUG = false; - - // TODO: Remove. - /** Whether to call binary dictionary dynamically updating methods. */ - public static boolean ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE = true; + private static final boolean DBG_STRESS_TEST = false; private static final int TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS = 100; + private static final int DEFAULT_MAX_UNIGRAM_COUNT = 10000; + private static final int DEFAULT_MAX_BIGRAM_COUNT = 10000; + /** * The maximum length of a word in this dictionary. */ protected static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH; - private static final int DICTIONARY_FORMAT_VERSION = 3; - - private static final String SUPPORTS_DYNAMIC_UPDATE = - FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE; - - /** - * A static map of update controllers, each of which records the time of accesses to a single - * binary dictionary file and tracks whether the file is regenerating. The key for this map is - * the filename and the value is the shared dictionary time recorder associated with that - * filename. - */ - private static final ConcurrentHashMap<String, DictionaryUpdateController> - sFilenameDictionaryUpdateControllerMap = CollectionUtils.newConcurrentHashMap(); - - private static final ConcurrentHashMap<String, PrioritizedSerialExecutor> - sFilenameExecutorMap = CollectionUtils.newConcurrentHashMap(); + private static final int DICTIONARY_FORMAT_VERSION = FormatSpec.VERSION4; /** The application context. */ protected final Context mContext; @@ -90,138 +78,109 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { */ private BinaryDictionary mBinaryDictionary; - // TODO: Remove and handle dictionaries in native code. - /** The in-memory dictionary used to generate the binary dictionary. */ - protected AbstractDictionaryWriter mDictionaryWriter; - /** - * The name of this dictionary, used as the filename for storing the binary dictionary. Multiple - * dictionary instances with the same filename is supported, with access controlled by - * DictionaryTimeRecorder. + * The name of this dictionary, used as a part of the filename for storing the binary + * dictionary. */ - private final String mFilename; + private final String mDictName; - /** Whether to support dynamically updating the dictionary */ - private final boolean mIsUpdatable; + /** Dictionary locale */ + private final Locale mLocale; - // TODO: remove, once dynamic operations is serialized - /** Controls updating the shared binary dictionary file across multiple instances. */ - private final DictionaryUpdateController mFilenameDictionaryUpdateController; + /** Dictionary file */ + private final File mDictFile; - // TODO: remove, once dynamic operations is serialized - /** Controls updating the local binary dictionary for this instance. */ - private final DictionaryUpdateController mPerInstanceDictionaryUpdateController = - new DictionaryUpdateController(); + /** Indicates whether a task for reloading the dictionary has been scheduled. */ + private final AtomicBoolean mIsReloading; - /* A extension for a binary dictionary file. */ - public static final String DICT_FILE_EXTENSION = ".dict"; + /** Indicates whether the current dictionary needs to be recreated. */ + private boolean mNeedsToRecreate; - private final AtomicReference<Runnable> mUnfinishedFlushingTask = - new AtomicReference<Runnable>(); + private final ReentrantReadWriteLock mLock; - /** - * Abstract method for loading the unigrams and bigrams of a given dictionary in a background - * thread. - */ - protected abstract void loadDictionaryAsync(); + private Map<String, String> mAdditionalAttributeMap = null; - /** - * Indicates that the source dictionary content has changed and a rebuild of the binary file is - * required. If it returns false, the next reload will only read the current binary dictionary - * from file. Note that the shared binary dictionary is locked when this is called. - */ - protected abstract boolean hasContentChanged(); + /* A extension for a binary dictionary file. */ + protected static final String DICT_FILE_EXTENSION = ".dict"; /** - * Gets the dictionary update controller for the given filename. + * Abstract method for loading initial contents of a given dictionary. */ - private static DictionaryUpdateController getDictionaryUpdateController( - String filename) { - DictionaryUpdateController recorder = sFilenameDictionaryUpdateControllerMap.get(filename); - if (recorder == null) { - synchronized(sFilenameDictionaryUpdateControllerMap) { - recorder = new DictionaryUpdateController(); - sFilenameDictionaryUpdateControllerMap.put(filename, recorder); - } - } - return recorder; + protected abstract void loadInitialContentsLocked(); + + private boolean matchesExpectedBinaryDictFormatVersionForThisType(final int formatVersion) { + return formatVersion == FormatSpec.VERSION4; } - /** - * Gets the executor for the given filename. - */ - private static PrioritizedSerialExecutor getExecutor(final String filename) { - PrioritizedSerialExecutor executor = sFilenameExecutorMap.get(filename); - if (executor == null) { - synchronized(sFilenameExecutorMap) { - executor = new PrioritizedSerialExecutor(); - sFilenameExecutorMap.put(filename, executor); - } - } - return executor; + private boolean needsToMigrateDictionary(final int formatVersion) { + // When we bump up the dictionary format version, the old version should be added to here + // for supporting migration. Note that native code has to support reading such formats. + return formatVersion == FormatSpec.VERSION4_ONLY_FOR_TESTING; } - private static AbstractDictionaryWriter getDictionaryWriter(final Context context, - final String dictType, final boolean isDynamicPersonalizationDictionary) { - if (isDynamicPersonalizationDictionary) { - if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { - return null; - } else { - return new DynamicPersonalizationDictionaryWriter(context, dictType); - } - } else { - return new DictionaryWriter(context, dictType); - } + public boolean isValidDictionaryLocked() { + return mBinaryDictionary.isValidDictionary(); } /** * Creates a new expandable binary dictionary. * * @param context The application context of the parent. - * @param filename The filename for this binary dictionary. Multiple dictionaries with the same - * filename is supported. + * @param dictName The name of the dictionary. Multiple instances with the same + * name is supported. + * @param locale the dictionary locale. * @param dictType the dictionary type, as a human-readable string - * @param isUpdatable whether to support dynamically updating the dictionary. Please note that - * dynamic dictionary has negative effects on memory space and computation time. + * @param dictFile dictionary file path. if null, use default dictionary path based on + * dictionary type. */ - public ExpandableBinaryDictionary(final Context context, final String filename, - final String dictType, final boolean isUpdatable) { + public ExpandableBinaryDictionary(final Context context, final String dictName, + final Locale locale, final String dictType, final File dictFile) { super(dictType); - mFilename = filename; + mDictName = dictName; mContext = context; - mIsUpdatable = isUpdatable; + mLocale = locale; + mDictFile = getDictFile(context, dictName, dictFile); mBinaryDictionary = null; - mFilenameDictionaryUpdateController = getDictionaryUpdateController(filename); - // Currently, only dynamic personalization dictionary is updatable. - mDictionaryWriter = getDictionaryWriter(context, dictType, isUpdatable); + mIsReloading = new AtomicBoolean(); + mNeedsToRecreate = false; + mLock = new ReentrantReadWriteLock(); } - protected static String getFilenameWithLocale(final String name, final String localeStr) { - return name + "." + localeStr + DICT_FILE_EXTENSION; + public static File getDictFile(final Context context, final String dictName, + final File dictFile) { + return (dictFile != null) ? dictFile + : new File(context.getFilesDir(), dictName + DICT_FILE_EXTENSION); } - /** - * Closes and cleans up the binary dictionary. - */ - @Override - public void close() { - getExecutor(mFilename).execute(new Runnable() { + public static String getDictName(final String name, final Locale locale, + final File dictFile) { + return dictFile != null ? dictFile.getName() : name + "." + locale.toString(); + } + + private void asyncExecuteTaskWithWriteLock(final Runnable task) { + asyncExecuteTaskWithLock(mLock.writeLock(), task); + } + + private void asyncExecuteTaskWithLock(final Lock lock, final Runnable task) { + ExecutorUtils.getExecutor(mDictName).execute(new Runnable() { @Override public void run() { - if (mBinaryDictionary!= null) { - mBinaryDictionary.close(); - mBinaryDictionary = null; - } - if (mDictionaryWriter != null) { - mDictionaryWriter.close(); + lock.lock(); + try { + task.run(); + } finally { + lock.unlock(); } } }); } - protected void closeBinaryDictionary() { - // Ensure that no other threads are accessing the local binary dictionary. - getExecutor(mFilename).execute(new Runnable() { + /** + * Closes and cleans up the binary dictionary. + */ + @Override + public void close() { + asyncExecuteTaskWithWriteLock(new Runnable() { @Override public void run() { if (mBinaryDictionary != null) { @@ -233,504 +192,482 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { } protected Map<String, String> getHeaderAttributeMap() { - HashMap<String, String> attributeMap = new HashMap<String, String>(); - attributeMap.put(FormatSpec.FileHeader.SUPPORTS_DYNAMIC_UPDATE_ATTRIBUTE, - SUPPORTS_DYNAMIC_UPDATE); - attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE, mFilename); + HashMap<String, String> attributeMap = new HashMap<>(); + if (mAdditionalAttributeMap != null) { + attributeMap.putAll(mAdditionalAttributeMap); + } + attributeMap.put(DictionaryHeader.DICTIONARY_ID_KEY, mDictName); + attributeMap.put(DictionaryHeader.DICTIONARY_LOCALE_KEY, mLocale.toString()); + attributeMap.put(DictionaryHeader.DICTIONARY_VERSION_KEY, + String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()))); + attributeMap.put(DictionaryHeader.MAX_UNIGRAM_COUNT_KEY, + String.valueOf(DEFAULT_MAX_UNIGRAM_COUNT)); + attributeMap.put(DictionaryHeader.MAX_BIGRAM_COUNT_KEY, + String.valueOf(DEFAULT_MAX_BIGRAM_COUNT)); return attributeMap; } - protected void clear() { - getExecutor(mFilename).execute(new Runnable() { + private void removeBinaryDictionary() { + asyncExecuteTaskWithWriteLock(new Runnable() { @Override public void run() { - if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE && mDictionaryWriter == null) { - mBinaryDictionary.close(); - final File file = new File(mContext.getFilesDir(), mFilename); - BinaryDictionary.createEmptyDictFile(file.getAbsolutePath(), - DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap()); - mBinaryDictionary = new BinaryDictionary( - file.getAbsolutePath(), 0 /* offset */, file.length(), - true /* useFullEditDistance */, null, mDictType, mIsUpdatable); - } else { - mDictionaryWriter.clear(); - } + removeBinaryDictionaryLocked(); } }); } - /** - * Adds a word unigram to the dictionary. Used for loading a dictionary. - * @param word The word to add. - * @param shortcutTarget A shortcut target for this word, or null if none. - * @param frequency The frequency for this unigram. - * @param shortcutFreq The frequency of the shortcut (0~15, with 15 = whitelist). Ignored - * if shortcutTarget is null. - * @param isNotAWord true if this is not a word, i.e. shortcut only. - */ - protected void addWord(final String word, final String shortcutTarget, - final int frequency, final int shortcutFreq, final boolean isNotAWord) { - mDictionaryWriter.addUnigramWord(word, shortcutTarget, frequency, shortcutFreq, isNotAWord); + private void removeBinaryDictionaryLocked() { + if (mBinaryDictionary != null) { + mBinaryDictionary.close(); + } + if (mDictFile.exists() && !FileUtils.deleteRecursively(mDictFile)) { + Log.e(TAG, "Can't remove a file: " + mDictFile.getName()); + } + mBinaryDictionary = null; } - /** - * Adds a word bigram in the dictionary. Used for loading a dictionary. - */ - protected void addBigram(final String prevWord, final String word, final int frequency, - final long lastModifiedTime) { - mDictionaryWriter.addBigramWords(prevWord, word, frequency, true /* isValid */, - lastModifiedTime); + private void openBinaryDictionaryLocked() { + mBinaryDictionary = new BinaryDictionary( + mDictFile.getAbsolutePath(), 0 /* offset */, mDictFile.length(), + true /* useFullEditDistance */, mLocale, mDictType, true /* isUpdatable */); + } + + private void createOnMemoryBinaryDictionaryLocked() { + mBinaryDictionary = new BinaryDictionary( + mDictFile.getAbsolutePath(), true /* useFullEditDistance */, mLocale, mDictType, + DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap()); + } + + public void clear() { + asyncExecuteTaskWithWriteLock(new Runnable() { + @Override + public void run() { + removeBinaryDictionaryLocked(); + createOnMemoryBinaryDictionaryLocked(); + } + }); } /** * Check whether GC is needed and run GC if required. */ protected void runGCIfRequired(final boolean mindsBlockByGC) { - if (!ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) return; - getExecutor(mFilename).execute(new Runnable() { + asyncExecuteTaskWithWriteLock(new Runnable() { @Override public void run() { - runGCIfRequiredInternalLocked(mindsBlockByGC); + if (mBinaryDictionary == null) { + return; + } + runGCIfRequiredLocked(mindsBlockByGC); } }); } - private void runGCIfRequiredInternalLocked(final boolean mindsBlockByGC) { - if (!ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) return; - // Calls to needsToRunGC() need to be serialized. + protected void runGCIfRequiredLocked(final boolean mindsBlockByGC) { if (mBinaryDictionary.needsToRunGC(mindsBlockByGC)) { - if (setIsRegeneratingIfNotRegenerating()) { - // Run GC after currently existing time sensitive operations. - getExecutor(mFilename).executePrioritized(new Runnable() { - @Override - public void run() { - try { - mBinaryDictionary.flushWithGC(); - } finally { - mFilenameDictionaryUpdateController.mIsRegenerating.set(false); - } - } - }); - } + mBinaryDictionary.flushWithGC(); } } /** - * Dynamically adds a word unigram to the dictionary. May overwrite an existing entry. + * Adds unigram information of a word to the dictionary. May overwrite an existing entry. */ - protected void addWordDynamically(final String word, final String shortcutTarget, - final int frequency, final int shortcutFreq, final boolean isNotAWord) { - if (!mIsUpdatable) { - Log.w(TAG, "addWordDynamically is called for non-updatable dictionary: " + mFilename); - return; - } - getExecutor(mFilename).execute(new Runnable() { + public void addUnigramEntryWithCheckingDistracter(final String word, final int frequency, + final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord, + final boolean isBlacklisted, final int timestamp, + final DistracterFilter distracterFilter) { + reloadDictionaryIfRequired(); + asyncExecuteTaskWithWriteLock(new Runnable() { @Override public void run() { - if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { - runGCIfRequiredInternalLocked(true /* mindsBlockByGC */); - mBinaryDictionary.addUnigramWord(word, frequency); - } else { - // TODO: Remove. - mDictionaryWriter.addUnigramWord(word, shortcutTarget, frequency, shortcutFreq, - isNotAWord); + if (mBinaryDictionary == null) { + return; + } + if (distracterFilter.isDistracterToWordsInDictionaries( + PrevWordsInfo.EMPTY_PREV_WORDS_INFO, word, mLocale)) { + // The word is a distracter. + return; } + runGCIfRequiredLocked(true /* mindsBlockByGC */); + addUnigramLocked(word, frequency, shortcutTarget, shortcutFreq, + isNotAWord, isBlacklisted, timestamp); } }); } + protected void addUnigramLocked(final String word, final int frequency, + final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord, + final boolean isBlacklisted, final int timestamp) { + if (!mBinaryDictionary.addUnigramEntry(word, frequency, shortcutTarget, shortcutFreq, + false /* isBeginningOfSentence */, isNotAWord, isBlacklisted, timestamp)) { + Log.e(TAG, "Cannot add unigram entry. word: " + word); + } + } + /** - * Dynamically adds a word bigram in the dictionary. May overwrite an existing entry. + * Dynamically remove the unigram entry from the dictionary. */ - protected void addBigramDynamically(final String word0, final String word1, - final int frequency, final boolean isValid) { - if (!mIsUpdatable) { - Log.w(TAG, "addBigramDynamically is called for non-updatable dictionary: " - + mFilename); - return; - } - getExecutor(mFilename).execute(new Runnable() { + public void removeUnigramEntryDynamically(final String word) { + reloadDictionaryIfRequired(); + asyncExecuteTaskWithWriteLock(new Runnable() { @Override public void run() { - if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { - runGCIfRequiredInternalLocked(true /* mindsBlockByGC */); - mBinaryDictionary.addBigramWords(word0, word1, frequency); - } else { - // TODO: Remove. - mDictionaryWriter.addBigramWords(word0, word1, frequency, isValid, - 0 /* lastTouchedTime */); + if (mBinaryDictionary == null) { + return; + } + runGCIfRequiredLocked(true /* mindsBlockByGC */); + if (!mBinaryDictionary.removeUnigramEntry(word)) { + if (DEBUG) { + Log.i(TAG, "Cannot remove unigram entry: " + word); + } } } }); } /** - * Dynamically remove a word bigram in the dictionary. + * Adds n-gram information of a word to the dictionary. May overwrite an existing entry. */ - protected void removeBigramDynamically(final String word0, final String word1) { - if (!mIsUpdatable) { - Log.w(TAG, "removeBigramDynamically is called for non-updatable dictionary: " - + mFilename); - return; + public void addNgramEntry(final PrevWordsInfo prevWordsInfo, final String word, + final int frequency, final int timestamp) { + reloadDictionaryIfRequired(); + asyncExecuteTaskWithWriteLock(new Runnable() { + @Override + public void run() { + if (mBinaryDictionary == null) { + return; + } + runGCIfRequiredLocked(true /* mindsBlockByGC */); + addNgramEntryLocked(prevWordsInfo, word, frequency, timestamp); + } + }); + } + + protected void addNgramEntryLocked(final PrevWordsInfo prevWordsInfo, final String word, + final int frequency, final int timestamp) { + if (!mBinaryDictionary.addNgramEntry(prevWordsInfo, word, frequency, timestamp)) { + if (DEBUG) { + Log.i(TAG, "Cannot add n-gram entry."); + Log.i(TAG, " PrevWordsInfo: " + prevWordsInfo + ", word: " + word); + } } - getExecutor(mFilename).execute(new Runnable() { + } + + /** + * Dynamically remove the n-gram entry in the dictionary. + */ + @UsedForTesting + public void removeNgramDynamically(final PrevWordsInfo prevWordsInfo, final String word) { + reloadDictionaryIfRequired(); + asyncExecuteTaskWithWriteLock(new Runnable() { @Override public void run() { - if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { - runGCIfRequiredInternalLocked(true /* mindsBlockByGC */); - mBinaryDictionary.removeBigramWords(word0, word1); - } else { - // TODO: Remove. - mDictionaryWriter.removeBigramWords(word0, word1); + if (mBinaryDictionary == null) { + return; + } + runGCIfRequiredLocked(true /* mindsBlockByGC */); + if (!mBinaryDictionary.removeNgramEntry(prevWordsInfo, word)) { + if (DEBUG) { + Log.i(TAG, "Cannot remove n-gram entry."); + Log.i(TAG, " PrevWordsInfo: " + prevWordsInfo + ", word: " + word); + } } } }); } - @Override - public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer, - final String prevWord, final ProximityInfo proximityInfo, - final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, - final int sessionId) { + public interface AddMultipleDictionaryEntriesCallback { + public void onFinished(); + } + + /** + * Dynamically add multiple entries to the dictionary. + */ + public void addMultipleDictionaryEntriesDynamically( + final ArrayList<LanguageModelParam> languageModelParams, + final AddMultipleDictionaryEntriesCallback callback) { reloadDictionaryIfRequired(); - if (isRegenerating()) { - return null; - } - final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList(); - final AsyncResultHolder<ArrayList<SuggestedWordInfo>> holder = - new AsyncResultHolder<ArrayList<SuggestedWordInfo>>(); - getExecutor(mFilename).executePrioritized(new Runnable() { + asyncExecuteTaskWithWriteLock(new Runnable() { @Override public void run() { - if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { + try { if (mBinaryDictionary == null) { - holder.set(null); return; } - final ArrayList<SuggestedWordInfo> binarySuggestion = - mBinaryDictionary.getSuggestionsWithSessionId(composer, prevWord, - proximityInfo, blockOffensiveWords, additionalFeaturesOptions, - sessionId); - holder.set(binarySuggestion); - } else { - final ArrayList<SuggestedWordInfo> inMemDictSuggestion = - composer.isBatchMode() ? null : - mDictionaryWriter.getSuggestionsWithSessionId(composer, - prevWord, proximityInfo, blockOffensiveWords, - additionalFeaturesOptions, sessionId); - // TODO: Remove checking mIsUpdatable and use native suggestion. - if (mBinaryDictionary != null && !mIsUpdatable) { - final ArrayList<SuggestedWordInfo> binarySuggestion = - mBinaryDictionary.getSuggestionsWithSessionId(composer, prevWord, - proximityInfo, blockOffensiveWords, - additionalFeaturesOptions, sessionId); - if (inMemDictSuggestion == null) { - holder.set(binarySuggestion); - } else if (binarySuggestion == null) { - holder.set(inMemDictSuggestion); - } else { - binarySuggestion.addAll(inMemDictSuggestion); - holder.set(binarySuggestion); - } - } else { - holder.set(inMemDictSuggestion); + mBinaryDictionary.addMultipleDictionaryEntries( + languageModelParams.toArray( + new LanguageModelParam[languageModelParams.size()])); + } finally { + if (callback != null) { + callback.onFinished(); } } } }); - return holder.get(null, TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS); } @Override public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, - final String prevWord, final ProximityInfo proximityInfo, - final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) { - return getSuggestionsWithSessionId(composer, prevWord, proximityInfo, blockOffensiveWords, - additionalFeaturesOptions, 0 /* sessionId */); + final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, + final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, + final int sessionId, final float[] inOutLanguageWeight) { + reloadDictionaryIfRequired(); + boolean lockAcquired = false; + try { + lockAcquired = mLock.readLock().tryLock( + TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS, TimeUnit.MILLISECONDS); + if (lockAcquired) { + if (mBinaryDictionary == null) { + return null; + } + final ArrayList<SuggestedWordInfo> suggestions = + mBinaryDictionary.getSuggestions(composer, prevWordsInfo, proximityInfo, + blockOffensiveWords, additionalFeaturesOptions, sessionId, + inOutLanguageWeight); + if (mBinaryDictionary.isCorrupted()) { + Log.i(TAG, "Dictionary (" + mDictName +") is corrupted. " + + "Remove and regenerate it."); + removeBinaryDictionary(); + } + return suggestions; + } + } catch (final InterruptedException e) { + Log.e(TAG, "Interrupted tryLock() in getSuggestionsWithSessionId().", e); + } finally { + if (lockAcquired) { + mLock.readLock().unlock(); + } + } + return null; } @Override - public boolean isValidWord(final String word) { + public boolean isInDictionary(final String word) { reloadDictionaryIfRequired(); - return isValidWordInner(word); - } - - protected boolean isValidWordInner(final String word) { - if (isRegenerating()) { - return false; - } - final AsyncResultHolder<Boolean> holder = new AsyncResultHolder<Boolean>(); - getExecutor(mFilename).executePrioritized(new Runnable() { - @Override - public void run() { - holder.set(isValidWordLocked(word)); + boolean lockAcquired = false; + try { + lockAcquired = mLock.readLock().tryLock( + TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS, TimeUnit.MILLISECONDS); + if (lockAcquired) { + if (mBinaryDictionary == null) { + return false; + } + return isInDictionaryLocked(word); } - }); - return holder.get(false, TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS); + } catch (final InterruptedException e) { + Log.e(TAG, "Interrupted tryLock() in isInDictionary().", e); + } finally { + if (lockAcquired) { + mLock.readLock().unlock(); + } + } + return false; } - protected boolean isValidWordLocked(final String word) { + protected boolean isInDictionaryLocked(final String word) { if (mBinaryDictionary == null) return false; - return mBinaryDictionary.isValidWord(word); + return mBinaryDictionary.isInDictionary(word); } - protected boolean isValidBigramLocked(final String word1, final String word2) { - if (mBinaryDictionary == null) return false; - return mBinaryDictionary.isValidBigram(word1, word2); + @Override + public int getMaxFrequencyOfExactMatches(final String word) { + reloadDictionaryIfRequired(); + boolean lockAcquired = false; + try { + lockAcquired = mLock.readLock().tryLock( + TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS, TimeUnit.MILLISECONDS); + if (lockAcquired) { + if (mBinaryDictionary == null) { + return NOT_A_PROBABILITY; + } + return mBinaryDictionary.getMaxFrequencyOfExactMatches(word); + } + } catch (final InterruptedException e) { + Log.e(TAG, "Interrupted tryLock() in getMaxFrequencyOfExactMatches().", e); + } finally { + if (lockAcquired) { + mLock.readLock().unlock(); + } + } + return NOT_A_PROBABILITY; } - /** - * Load the current binary dictionary from internal storage in a background thread. If no binary - * dictionary exists, this method will generate one. - */ - protected void loadDictionary() { - mPerInstanceDictionaryUpdateController.mLastUpdateRequestTime = SystemClock.uptimeMillis(); - reloadDictionaryIfRequired(); + + protected boolean isValidNgramLocked(final PrevWordsInfo prevWordsInfo, final String word) { + if (mBinaryDictionary == null) return false; + return mBinaryDictionary.isValidNgram(prevWordsInfo, word); } /** * Loads the current binary dictionary from internal storage. Assumes the dictionary file * exists. */ - private void loadBinaryDictionary() { - if (DEBUG) { - Log.d(TAG, "Loading binary dictionary: " + mFilename + " request=" - + mFilenameDictionaryUpdateController.mLastUpdateRequestTime + " update=" - + mFilenameDictionaryUpdateController.mLastUpdateTime); + private void loadBinaryDictionaryLocked() { + if (DBG_STRESS_TEST) { + // Test if this class does not cause problems when it takes long time to load binary + // dictionary. + try { + Log.w(TAG, "Start stress in loading: " + mDictName); + Thread.sleep(15000); + Log.w(TAG, "End stress in loading"); + } catch (InterruptedException e) { + } } - - final File file = new File(mContext.getFilesDir(), mFilename); - final String filename = file.getAbsolutePath(); - final long length = file.length(); - - // Build the new binary dictionary - final BinaryDictionary newBinaryDictionary = new BinaryDictionary(filename, 0 /* offset */, - length, true /* useFullEditDistance */, null, mDictType, mIsUpdatable); - - // Ensure all threads accessing the current dictionary have finished before - // swapping in the new one. - // TODO: Ensure multi-thread assignment of mBinaryDictionary. final BinaryDictionary oldBinaryDictionary = mBinaryDictionary; - getExecutor(mFilename).executePrioritized(new Runnable() { - @Override - public void run() { - mBinaryDictionary = newBinaryDictionary; - if (oldBinaryDictionary != null) { - oldBinaryDictionary.close(); - } + openBinaryDictionaryLocked(); + if (oldBinaryDictionary != null) { + oldBinaryDictionary.close(); + } + if (mBinaryDictionary.isValidDictionary() + && needsToMigrateDictionary(mBinaryDictionary.getFormatVersion())) { + if (!mBinaryDictionary.migrateTo(DICTIONARY_FORMAT_VERSION)) { + Log.e(TAG, "Dictionary migration failed: " + mDictName); + removeBinaryDictionaryLocked(); } - }); + } } /** - * Abstract method for checking if it is required to reload the dictionary before writing - * a binary dictionary. + * Create a new binary dictionary and load initial contents. */ - abstract protected boolean needsToReloadBeforeWriting(); - - /** - * Writes a new binary dictionary based on the contents of the fusion dictionary. - */ - private void writeBinaryDictionary() { - if (DEBUG) { - Log.d(TAG, "Generating binary dictionary: " + mFilename + " request=" - + mFilenameDictionaryUpdateController.mLastUpdateRequestTime + " update=" - + mFilenameDictionaryUpdateController.mLastUpdateTime); - } - if (needsToReloadBeforeWriting()) { - mDictionaryWriter.clear(); - loadDictionaryAsync(); - mDictionaryWriter.write(mFilename, getHeaderAttributeMap()); - } else { - if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { - if (mBinaryDictionary == null || !mBinaryDictionary.isValidDictionary()) { - final File file = new File(mContext.getFilesDir(), mFilename); - BinaryDictionary.createEmptyDictFile(file.getAbsolutePath(), - DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap()); - } else { - if (mBinaryDictionary.needsToRunGC(false /* mindsBlockByGC */)) { - mBinaryDictionary.flushWithGC(); - } else { - mBinaryDictionary.flush(); - } - } - } else { - mDictionaryWriter.write(mFilename, getHeaderAttributeMap()); - } - } + private void createNewDictionaryLocked() { + removeBinaryDictionaryLocked(); + createOnMemoryBinaryDictionaryLocked(); + loadInitialContentsLocked(); + // Run GC and flush to file when initial contents have been loaded. + mBinaryDictionary.flushWithGCIfHasUpdated(); } /** - * Marks that the dictionary is out of date and requires a reload. + * Marks that the dictionary needs to be recreated. * - * @param requiresRebuild Indicates that the source dictionary content has changed and a rebuild - * of the binary file is required. If not true, the next reload process will only read - * the current binary dictionary from file. */ - protected void setRequiresReload(final boolean requiresRebuild) { - final long time = SystemClock.uptimeMillis(); - mPerInstanceDictionaryUpdateController.mLastUpdateRequestTime = time; - mFilenameDictionaryUpdateController.mLastUpdateRequestTime = time; - if (DEBUG) { - Log.d(TAG, "Reload request: " + mFilename + ": request=" + time + " update=" - + mFilenameDictionaryUpdateController.mLastUpdateTime); - } + protected void setNeedsToRecreate() { + mNeedsToRecreate = true; } /** - * Reloads the dictionary if required. + * Load the current binary dictionary from internal storage. If the dictionary file doesn't + * exists or needs to be regenerated, the new dictionary file will be asynchronously generated. + * However, the dictionary itself is accessible even before the new dictionary file is actually + * generated. It may return a null result for getSuggestions() in that case by design. */ public final void reloadDictionaryIfRequired() { if (!isReloadRequired()) return; - if (setIsRegeneratingIfNotRegenerating()) { - reloadDictionary(); - } + asyncReloadDictionary(); } /** * Returns whether a dictionary reload is required. */ private boolean isReloadRequired() { - return mBinaryDictionary == null || mPerInstanceDictionaryUpdateController.isOutOfDate(); - } - - private boolean isRegenerating() { - return mFilenameDictionaryUpdateController.mIsRegenerating.get(); - } - - // Returns whether the dictionary can be regenerated. - private boolean setIsRegeneratingIfNotRegenerating() { - return mFilenameDictionaryUpdateController.mIsRegenerating.compareAndSet( - false /* expect */ , true /* update */); + return mBinaryDictionary == null || mNeedsToRecreate; } /** - * Reloads the dictionary. Access is controlled on a per dictionary file basis and supports - * concurrent calls from multiple instances that share the same dictionary file. + * Reloads the dictionary. Access is controlled on a per dictionary file basis. */ - private final void reloadDictionary() { - // Ensure that only one thread attempts to read or write to the shared binary dictionary - // file at the same time. - getExecutor(mFilename).execute(new Runnable() { - @Override - public void run() { - try { - final long time = SystemClock.uptimeMillis(); - final boolean dictionaryFileExists = dictionaryFileExists(); - if (mFilenameDictionaryUpdateController.isOutOfDate() - || !dictionaryFileExists) { - // If the shared dictionary file does not exist or is out of date, the - // first instance that acquires the lock will generate a new one. - if (hasContentChanged() || !dictionaryFileExists) { - // If the source content has changed or the dictionary does not exist, - // rebuild the binary dictionary. Empty dictionaries are supported (in - // the case where loadDictionaryAsync() adds nothing) in order to - // provide a uniform framework. - mFilenameDictionaryUpdateController.mLastUpdateTime = time; - writeBinaryDictionary(); - loadBinaryDictionary(); - } else { - // If not, the reload request was unnecessary so revert - // LastUpdateRequestTime to LastUpdateTime. - mFilenameDictionaryUpdateController.mLastUpdateRequestTime = - mFilenameDictionaryUpdateController.mLastUpdateTime; + private final void asyncReloadDictionary() { + if (mIsReloading.compareAndSet(false, true)) { + asyncExecuteTaskWithWriteLock(new Runnable() { + @Override + public void run() { + try { + if (!mDictFile.exists() || mNeedsToRecreate) { + // If the dictionary file does not exist or contents have been updated, + // generate a new one. + createNewDictionaryLocked(); + } else if (mBinaryDictionary == null) { + // Otherwise, load the existing dictionary. + loadBinaryDictionaryLocked(); + if (mBinaryDictionary != null && !(isValidDictionaryLocked() + // TODO: remove the check below + && matchesExpectedBinaryDictFormatVersionForThisType( + mBinaryDictionary.getFormatVersion()))) { + // Binary dictionary or its format version is not valid. Regenerate + // the dictionary file. createNewDictionaryLocked will remove the + // existing files if appropriate. + createNewDictionaryLocked(); + } } - } else if (mBinaryDictionary == null || - mPerInstanceDictionaryUpdateController.mLastUpdateTime - < mFilenameDictionaryUpdateController.mLastUpdateTime) { - // Otherwise, if the local dictionary is older than the shared dictionary, - // load the shared dictionary. - loadBinaryDictionary(); + mNeedsToRecreate = false; + } finally { + mIsReloading.set(false); } - if (mBinaryDictionary != null && !mBinaryDictionary.isValidDictionary()) { - // Binary dictionary is not valid. Regenerate the dictionary file. - mFilenameDictionaryUpdateController.mLastUpdateTime = time; - writeBinaryDictionary(); - loadBinaryDictionary(); - } - mPerInstanceDictionaryUpdateController.mLastUpdateTime = time; - } finally { - mFilenameDictionaryUpdateController.mIsRegenerating.set(false); } - } - }); - } - - // TODO: cache the file's existence so that we avoid doing a disk access each time. - private boolean dictionaryFileExists() { - final File file = new File(mContext.getFilesDir(), mFilename); - return file.exists(); + }); + } } /** - * Load the dictionary to memory. + * Flush binary dictionary to dictionary file. */ - protected void asyncLoadDictionaryToMemory() { - getExecutor(mFilename).executePrioritized(new Runnable() { + public void asyncFlushBinaryDictionary() { + asyncExecuteTaskWithWriteLock(new Runnable() { @Override public void run() { - if (!ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { - loadDictionaryAsync(); + if (mBinaryDictionary == null) { + return; + } + if (mBinaryDictionary.needsToRunGC(false /* mindsBlockByGC */)) { + mBinaryDictionary.flushWithGC(); + } else { + mBinaryDictionary.flush(); } } }); } - /** - * Generate binary dictionary using DictionaryWriter. - */ - protected void asyncFlashAllBinaryDictionary() { - final Runnable newTask = new Runnable() { + @UsedForTesting + public void waitAllTasksForTests() { + final CountDownLatch countDownLatch = new CountDownLatch(1); + ExecutorUtils.getExecutor(mDictName).execute(new Runnable() { @Override public void run() { - writeBinaryDictionary(); + countDownLatch.countDown(); } - }; - final Runnable oldTask = mUnfinishedFlushingTask.getAndSet(newTask); - getExecutor(mFilename).replaceAndExecute(oldTask, newTask); - } - - /** - * For tracking whether the dictionary is out of date and the dictionary is regenerating. - * Can be shared across multiple dictionary instances that access the same filename. - */ - private static class DictionaryUpdateController { - public volatile long mLastUpdateTime = 0; - public volatile long mLastUpdateRequestTime = 0; - public volatile AtomicBoolean mIsRegenerating = new AtomicBoolean(); - - public boolean isOutOfDate() { - return (mLastUpdateRequestTime > mLastUpdateTime); + }); + try { + countDownLatch.await(); + } catch (InterruptedException e) { + Log.e(TAG, "Interrupted while waiting for finishing dictionary operations.", e); } } - // TODO: Implement native binary methods once the dynamic dictionary implementation is done. @UsedForTesting - public boolean isInDictionaryForTests(final String word) { - final AsyncResultHolder<Boolean> holder = new AsyncResultHolder<Boolean>(); - getExecutor(mFilename).executePrioritized(new Runnable() { + public void clearAndFlushDictionaryWithAdditionalAttributes( + final Map<String, String> attributeMap) { + mAdditionalAttributeMap = attributeMap; + clear(); + } + + public void dumpAllWordsForDebug() { + reloadDictionaryIfRequired(); + asyncExecuteTaskWithLock(mLock.readLock(), new Runnable() { @Override public void run() { - if (mDictType == Dictionary.TYPE_USER_HISTORY) { - if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { - holder.set(mBinaryDictionary.isValidWord(word)); - } else { - holder.set(((DynamicPersonalizationDictionaryWriter) mDictionaryWriter) - .isInBigramListForTests(word)); - } + Log.d(TAG, "Dump dictionary: " + mDictName); + try { + final DictionaryHeader header = mBinaryDictionary.getHeader(); + Log.d(TAG, "Format version: " + mBinaryDictionary.getFormatVersion()); + Log.d(TAG, CombinedFormatUtils.formatAttributeMap( + header.mDictionaryOptions.mAttributes)); + } catch (final UnsupportedFormatException e) { + Log.d(TAG, "Cannot fetch header information.", e); } + int token = 0; + do { + final BinaryDictionary.GetNextWordPropertyResult result = + mBinaryDictionary.getNextWordProperty(token); + final WordProperty wordProperty = result.mWordProperty; + if (wordProperty == null) { + Log.d(TAG, " dictionary is empty."); + break; + } + Log.d(TAG, wordProperty.toString()); + token = result.mNextToken; + } while (token != 0); } }); - return holder.get(false, TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS); - } - - @UsedForTesting - public void shutdownExecutorForTests() { - getExecutor(mFilename).shutdown(); - } - - @UsedForTesting - public boolean isTerminatedForTests() { - return getExecutor(mFilename).isTerminated(); } } diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java deleted file mode 100644 index 95c9bcab9..000000000 --- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java +++ /dev/null @@ -1,894 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin; - -import android.text.TextUtils; -import android.util.Log; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.keyboard.ProximityInfo; -import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; -import com.android.inputmethod.latin.utils.CollectionUtils; -import com.android.inputmethod.latin.utils.UserHistoryForgettingCurveUtils.ForgettingCurveParams; - -import java.util.ArrayList; -import java.util.LinkedList; - -/** - * Class for an in-memory dictionary that can grow dynamically and can - * be searched for suggestions and valid words. - */ -// TODO: Remove after binary dictionary supports dynamic update. -public class ExpandableDictionary extends Dictionary { - private static final String TAG = ExpandableDictionary.class.getSimpleName(); - /** - * The weight to give to a word if it's length is the same as the number of typed characters. - */ - private static final int FULL_WORD_SCORE_MULTIPLIER = 2; - - private char[] mWordBuilder = new char[Constants.DICTIONARY_MAX_WORD_LENGTH]; - private int mMaxDepth; - private int mInputLength; - - private static final class Node { - char mCode; - int mFrequency; - boolean mTerminal; - Node mParent; - NodeArray mChildren; - ArrayList<char[]> mShortcutTargets; - boolean mShortcutOnly; - LinkedList<NextWord> mNGrams; // Supports ngram - } - - private static final class NodeArray { - Node[] mData; - int mLength = 0; - private static final int INCREMENT = 2; - - NodeArray() { - mData = new Node[INCREMENT]; - } - - void add(final Node n) { - if (mLength + 1 > mData.length) { - Node[] tempData = new Node[mLength + INCREMENT]; - if (mLength > 0) { - System.arraycopy(mData, 0, tempData, 0, mLength); - } - mData = tempData; - } - mData[mLength++] = n; - } - } - - public interface NextWord { - public Node getWordNode(); - public int getFrequency(); - public ForgettingCurveParams getFcParams(); - public int notifyTypedAgainAndGetFrequency(); - } - - private static final class NextStaticWord implements NextWord { - public final Node mWord; - private final int mFrequency; - public NextStaticWord(Node word, int frequency) { - mWord = word; - mFrequency = frequency; - } - - @Override - public Node getWordNode() { - return mWord; - } - - @Override - public int getFrequency() { - return mFrequency; - } - - @Override - public ForgettingCurveParams getFcParams() { - return null; - } - - @Override - public int notifyTypedAgainAndGetFrequency() { - return mFrequency; - } - } - - private static final class NextHistoryWord implements NextWord { - public final Node mWord; - public final ForgettingCurveParams mFcp; - - public NextHistoryWord(Node word, ForgettingCurveParams fcp) { - mWord = word; - mFcp = fcp; - } - - @Override - public Node getWordNode() { - return mWord; - } - - @Override - public int getFrequency() { - return mFcp.getFrequency(); - } - - @Override - public ForgettingCurveParams getFcParams() { - return mFcp; - } - - @Override - public int notifyTypedAgainAndGetFrequency() { - return mFcp.notifyTypedAgainAndGetFrequency(); - } - } - - private NodeArray mRoots; - - private int[][] mCodes; - - public ExpandableDictionary(final String dictType) { - super(dictType); - clearDictionary(); - mCodes = new int[Constants.DICTIONARY_MAX_WORD_LENGTH][]; - } - - public int getMaxWordLength() { - return Constants.DICTIONARY_MAX_WORD_LENGTH; - } - - /** - * Add a word with an optional shortcut to the dictionary. - * @param word The word to add. - * @param shortcutTarget A shortcut target for this word, or null if none. - * @param frequency The frequency for this unigram. - * @param shortcutFreq The frequency of the shortcut (0~15, with 15 = whitelist). Ignored - * if shortcutTarget is null. - */ - public void addWord(final String word, final String shortcutTarget, final int frequency, - final int shortcutFreq) { - if (word.length() >= Constants.DICTIONARY_MAX_WORD_LENGTH) { - return; - } - addWordRec(mRoots, word, 0, shortcutTarget, frequency, shortcutFreq, null); - } - - /** - * Add a word, recursively searching for its correct place in the trie tree. - * @param children The node to recursively search for addition. Initially, the root of the tree. - * @param word The word to add. - * @param depth The current depth in the tree. - * @param shortcutTarget A shortcut target for this word, or null if none. - * @param frequency The frequency for this unigram. - * @param shortcutFreq The frequency of the shortcut (0~15, with 15 = whitelist). Ignored - * if shortcutTarget is null. - * @param parentNode The parent node, for up linking. Initially null, as the root has no parent. - */ - private void addWordRec(final NodeArray children, final String word, final int depth, - final String shortcutTarget, final int frequency, final int shortcutFreq, - final Node parentNode) { - final int wordLength = word.length(); - if (wordLength <= depth) return; - final char c = word.charAt(depth); - // Does children have the current character? - final int childrenLength = children.mLength; - Node childNode = null; - for (int i = 0; i < childrenLength; i++) { - final Node node = children.mData[i]; - if (node.mCode == c) { - childNode = node; - break; - } - } - final boolean isShortcutOnly = (null != shortcutTarget); - if (childNode == null) { - childNode = new Node(); - childNode.mCode = c; - childNode.mParent = parentNode; - childNode.mShortcutOnly = isShortcutOnly; - children.add(childNode); - } - if (wordLength == depth + 1) { - // Terminate this word - childNode.mTerminal = true; - if (isShortcutOnly) { - if (null == childNode.mShortcutTargets) { - childNode.mShortcutTargets = CollectionUtils.newArrayList(); - } - childNode.mShortcutTargets.add(shortcutTarget.toCharArray()); - } else { - childNode.mShortcutOnly = false; - } - childNode.mFrequency = Math.max(frequency, childNode.mFrequency); - if (childNode.mFrequency > 255) childNode.mFrequency = 255; - return; - } - if (childNode.mChildren == null) { - childNode.mChildren = new NodeArray(); - } - addWordRec(childNode.mChildren, word, depth + 1, shortcutTarget, frequency, shortcutFreq, - childNode); - } - - @Override - public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, - final String prevWord, final ProximityInfo proximityInfo, - final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) { - if (composer.size() > 1) { - if (composer.size() >= Constants.DICTIONARY_MAX_WORD_LENGTH) { - return null; - } - final ArrayList<SuggestedWordInfo> suggestions = - getWordsInner(composer, prevWord, proximityInfo); - return suggestions; - } else { - if (TextUtils.isEmpty(prevWord)) return null; - final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList(); - runBigramReverseLookUp(prevWord, suggestions); - return suggestions; - } - } - - private ArrayList<SuggestedWordInfo> getWordsInner(final WordComposer codes, - final String prevWordForBigrams, final ProximityInfo proximityInfo) { - final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList(); - mInputLength = codes.size(); - if (mCodes.length < mInputLength) mCodes = new int[mInputLength][]; - final InputPointers ips = codes.getInputPointers(); - final int[] xCoordinates = ips.getXCoordinates(); - final int[] yCoordinates = ips.getYCoordinates(); - // Cache the codes so that we don't have to lookup an array list - for (int i = 0; i < mInputLength; i++) { - // TODO: Calculate proximity info here. - if (mCodes[i] == null || mCodes[i].length < 1) { - mCodes[i] = new int[ProximityInfo.MAX_PROXIMITY_CHARS_SIZE]; - } - final int x = xCoordinates != null && i < xCoordinates.length ? - xCoordinates[i] : Constants.NOT_A_COORDINATE; - final int y = xCoordinates != null && i < yCoordinates.length ? - yCoordinates[i] : Constants.NOT_A_COORDINATE; - proximityInfo.fillArrayWithNearestKeyCodes(x, y, codes.getCodeAt(i), mCodes[i]); - } - mMaxDepth = mInputLength * 3; - getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1, 0, -1, suggestions); - for (int i = 0; i < mInputLength; i++) { - getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1, 0, i, suggestions); - } - return suggestions; - } - - @Override - public synchronized boolean isValidWord(final String word) { - final Node node = searchNode(mRoots, word, 0, word.length()); - // If node is null, we didn't find the word, so it's not valid. - // If node.mShortcutOnly is true, then it exists as a shortcut but not as a word, - // so that means it's not a valid word. - // If node.mShortcutOnly is false, then it exists as a word (it may also exist as - // a shortcut, but this does not matter), so it's a valid word. - return (node == null) ? false : !node.mShortcutOnly; - } - - public boolean removeBigram(final String word0, final String word1) { - // Refer to addOrSetBigram() about word1.toLowerCase() - final Node firstWord = searchWord(mRoots, word0.toLowerCase(), 0, null); - final Node secondWord = searchWord(mRoots, word1, 0, null); - LinkedList<NextWord> bigrams = firstWord.mNGrams; - NextWord bigramNode = null; - if (bigrams == null || bigrams.size() == 0) { - return false; - } else { - for (NextWord nw : bigrams) { - if (nw.getWordNode() == secondWord) { - bigramNode = nw; - break; - } - } - } - if (bigramNode == null) { - return false; - } - return bigrams.remove(bigramNode); - } - - /** - * Returns the word's frequency or -1 if not found - */ - @UsedForTesting - public int getWordFrequency(final String word) { - // Case-sensitive search - final Node node = searchNode(mRoots, word, 0, word.length()); - return (node == null) ? -1 : node.mFrequency; - } - - public NextWord getBigramWord(final String word0, final String word1) { - // Refer to addOrSetBigram() about word0.toLowerCase() - final Node firstWord = searchWord(mRoots, word0.toLowerCase(), 0, null); - final Node secondWord = searchWord(mRoots, word1, 0, null); - LinkedList<NextWord> bigrams = firstWord.mNGrams; - if (bigrams == null || bigrams.size() == 0) { - return null; - } else { - for (NextWord nw : bigrams) { - if (nw.getWordNode() == secondWord) { - return nw; - } - } - } - return null; - } - - private static int computeSkippedWordFinalFreq(final int freq, final int snr, - final int inputLength) { - // The computation itself makes sense for >= 2, but the == 2 case returns 0 - // anyway so we may as well test against 3 instead and return the constant - if (inputLength >= 3) { - return (freq * snr * (inputLength - 2)) / (inputLength - 1); - } else { - return 0; - } - } - - /** - * Helper method to add a word and its shortcuts. - * - * @param node the terminal node - * @param word the word to insert, as an array of code points - * @param depth the depth of the node in the tree - * @param finalFreq the frequency for this word - * @param suggestions the suggestion collection to add the suggestions to - * @return whether there is still space for more words. - */ - private boolean addWordAndShortcutsFromNode(final Node node, final char[] word, final int depth, - final int finalFreq, final ArrayList<SuggestedWordInfo> suggestions) { - if (finalFreq > 0 && !node.mShortcutOnly) { - // Use KIND_CORRECTION always. This dictionary does not really have a notion of - // COMPLETION against CORRECTION; we could artificially add one by looking at - // the respective size of the typed word and the suggestion if it matters sometime - // in the future. - suggestions.add(new SuggestedWordInfo(new String(word, 0, depth + 1), finalFreq, - SuggestedWordInfo.KIND_CORRECTION, this /* sourceDict */, - SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, - SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); - if (suggestions.size() >= Suggest.MAX_SUGGESTIONS) return false; - } - if (null != node.mShortcutTargets) { - final int length = node.mShortcutTargets.size(); - for (int shortcutIndex = 0; shortcutIndex < length; ++shortcutIndex) { - final char[] shortcut = node.mShortcutTargets.get(shortcutIndex); - suggestions.add(new SuggestedWordInfo(new String(shortcut, 0, shortcut.length), - finalFreq, SuggestedWordInfo.KIND_SHORTCUT, this /* sourceDict */, - SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, - SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); - if (suggestions.size() > Suggest.MAX_SUGGESTIONS) return false; - } - } - return true; - } - - /** - * Recursively traverse the tree for words that match the input. Input consists of - * a list of arrays. Each item in the list is one input character position. An input - * character is actually an array of multiple possible candidates. This function is not - * optimized for speed, assuming that the user dictionary will only be a few hundred words in - * size. - * @param roots node whose children have to be search for matches - * @param codes the input character codes - * @param word the word being composed as a possible match - * @param depth the depth of traversal - the length of the word being composed thus far - * @param completion whether the traversal is now in completion mode - meaning that we've - * exhausted the input and we're looking for all possible suffixes. - * @param snr current weight of the word being formed - * @param inputIndex position in the input characters. This can be off from the depth in - * case we skip over some punctuations such as apostrophe in the traversal. That is, if you type - * "wouldve", it could be matching "would've", so the depth will be one more than the - * inputIndex - * @param suggestions the list in which to add suggestions - */ - // TODO: Share this routine with the native code for BinaryDictionary - private void getWordsRec(final NodeArray roots, final WordComposer codes, final char[] word, - final int depth, final boolean completion, final int snr, final int inputIndex, - final int skipPos, final ArrayList<SuggestedWordInfo> suggestions) { - final int count = roots.mLength; - final int codeSize = mInputLength; - // Optimization: Prune out words that are too long compared to how much was typed. - if (depth > mMaxDepth) { - return; - } - final int[] currentChars; - if (codeSize <= inputIndex) { - currentChars = null; - } else { - currentChars = mCodes[inputIndex]; - } - - for (int i = 0; i < count; i++) { - final Node node = roots.mData[i]; - final char c = node.mCode; - final char lowerC = toLowerCase(c); - final boolean terminal = node.mTerminal; - final NodeArray children = node.mChildren; - final int freq = node.mFrequency; - if (completion || currentChars == null) { - word[depth] = c; - if (terminal) { - final int finalFreq; - if (skipPos < 0) { - finalFreq = freq * snr; - } else { - finalFreq = computeSkippedWordFinalFreq(freq, snr, mInputLength); - } - if (!addWordAndShortcutsFromNode(node, word, depth, finalFreq, suggestions)) { - // No space left in the queue, bail out - return; - } - } - if (children != null) { - getWordsRec(children, codes, word, depth + 1, true, snr, inputIndex, - skipPos, suggestions); - } - } else if ((c == Constants.CODE_SINGLE_QUOTE - && currentChars[0] != Constants.CODE_SINGLE_QUOTE) || depth == skipPos) { - // Skip the ' and continue deeper - word[depth] = c; - if (children != null) { - getWordsRec(children, codes, word, depth + 1, completion, snr, inputIndex, - skipPos, suggestions); - } - } else { - // Don't use alternatives if we're looking for missing characters - final int alternativesSize = skipPos >= 0 ? 1 : currentChars.length; - for (int j = 0; j < alternativesSize; j++) { - final int addedAttenuation = (j > 0 ? 1 : 2); - final int currentChar = currentChars[j]; - if (currentChar == Constants.NOT_A_CODE) { - break; - } - if (currentChar == lowerC || currentChar == c) { - word[depth] = c; - - if (codeSize == inputIndex + 1) { - if (terminal) { - final int finalFreq; - if (skipPos < 0) { - finalFreq = freq * snr * addedAttenuation - * FULL_WORD_SCORE_MULTIPLIER; - } else { - finalFreq = computeSkippedWordFinalFreq(freq, - snr * addedAttenuation, mInputLength); - } - if (!addWordAndShortcutsFromNode(node, word, depth, finalFreq, - suggestions)) { - // No space left in the queue, bail out - return; - } - } - if (children != null) { - getWordsRec(children, codes, word, depth + 1, - true, snr * addedAttenuation, inputIndex + 1, - skipPos, suggestions); - } - } else if (children != null) { - getWordsRec(children, codes, word, depth + 1, - false, snr * addedAttenuation, inputIndex + 1, - skipPos, suggestions); - } - } - } - } - } - } - - public int setBigramAndGetFrequency(final String word0, final String word1, - final int frequency) { - return setBigramAndGetFrequency(word0, word1, frequency, null /* unused */); - } - - public int setBigramAndGetFrequency(final String word0, final String word1, - final ForgettingCurveParams fcp) { - return setBigramAndGetFrequency(word0, word1, 0 /* unused */, fcp); - } - - /** - * Adds bigrams to the in-memory trie structure that is being used to retrieve any word - * @param word0 the first word of this bigram - * @param word1 the second word of this bigram - * @param frequency frequency for this bigram - * @param fcp an instance of ForgettingCurveParams to use for decay policy - * @return returns the final bigram frequency - */ - private int setBigramAndGetFrequency(final String word0, final String word1, - final int frequency, final ForgettingCurveParams fcp) { - if (TextUtils.isEmpty(word0)) { - Log.e(TAG, "Invalid bigram previous word: " + word0); - return frequency; - } - // We don't want results to be different according to case of the looked up left hand side - // word. We do want however to return the correct case for the right hand side. - // So we want to squash the case of the left hand side, and preserve that of the right - // hand side word. - final String word0Lower = word0.toLowerCase(); - if (TextUtils.isEmpty(word0Lower) || TextUtils.isEmpty(word1)) { - Log.e(TAG, "Invalid bigram pair: " + word0 + ", " + word0Lower + ", " + word1); - return frequency; - } - final Node firstWord = searchWord(mRoots, word0Lower, 0, null); - final Node secondWord = searchWord(mRoots, word1, 0, null); - LinkedList<NextWord> bigrams = firstWord.mNGrams; - if (bigrams == null || bigrams.size() == 0) { - firstWord.mNGrams = CollectionUtils.newLinkedList(); - bigrams = firstWord.mNGrams; - } else { - for (NextWord nw : bigrams) { - if (nw.getWordNode() == secondWord) { - return nw.notifyTypedAgainAndGetFrequency(); - } - } - } - if (fcp != null) { - // history - firstWord.mNGrams.add(new NextHistoryWord(secondWord, fcp)); - } else { - firstWord.mNGrams.add(new NextStaticWord(secondWord, frequency)); - } - return frequency; - } - - /** - * Searches for the word and add the word if it does not exist. - * @return Returns the terminal node of the word we are searching for. - */ - private Node searchWord(final NodeArray children, final String word, final int depth, - final Node parentNode) { - final int wordLength = word.length(); - final char c = word.charAt(depth); - // Does children have the current character? - final int childrenLength = children.mLength; - Node childNode = null; - for (int i = 0; i < childrenLength; i++) { - final Node node = children.mData[i]; - if (node.mCode == c) { - childNode = node; - break; - } - } - if (childNode == null) { - childNode = new Node(); - childNode.mCode = c; - childNode.mParent = parentNode; - children.add(childNode); - } - if (wordLength == depth + 1) { - // Terminate this word - childNode.mTerminal = true; - return childNode; - } - if (childNode.mChildren == null) { - childNode.mChildren = new NodeArray(); - } - return searchWord(childNode.mChildren, word, depth + 1, childNode); - } - - private void runBigramReverseLookUp(final String previousWord, - final ArrayList<SuggestedWordInfo> suggestions) { - // Search for the lowercase version of the word only, because that's where bigrams - // store their sons. - final Node prevWord = searchNode(mRoots, previousWord.toLowerCase(), 0, - previousWord.length()); - if (prevWord != null && prevWord.mNGrams != null) { - reverseLookUp(prevWord.mNGrams, suggestions); - } - } - - // Local to reverseLookUp, but do not allocate each time. - private final char[] mLookedUpString = new char[Constants.DICTIONARY_MAX_WORD_LENGTH]; - - /** - * reverseLookUp retrieves the full word given a list of terminal nodes and adds those words - * to the suggestions list passed as an argument. - * @param terminalNodes list of terminal nodes we want to add - * @param suggestions the suggestion collection to add the word to - */ - private void reverseLookUp(final LinkedList<NextWord> terminalNodes, - final ArrayList<SuggestedWordInfo> suggestions) { - Node node; - int freq; - for (NextWord nextWord : terminalNodes) { - node = nextWord.getWordNode(); - freq = nextWord.getFrequency(); - int index = Constants.DICTIONARY_MAX_WORD_LENGTH; - do { - --index; - mLookedUpString[index] = node.mCode; - node = node.mParent; - } while (node != null && index > 0); - - // If node is null, we have a word longer than MAX_WORD_LENGTH in the dictionary. - // It's a little unclear how this can happen, but just in case it does it's safer - // to ignore the word in this case. - if (freq >= 0 && node == null) { - suggestions.add(new SuggestedWordInfo(new String(mLookedUpString, index, - Constants.DICTIONARY_MAX_WORD_LENGTH - index), - freq, SuggestedWordInfo.KIND_CORRECTION, this /* sourceDict */, - SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, - SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); - } - } - } - - /** - * Recursively search for the terminal node of the word. - * - * One iteration takes the full word to search for and the current index of the recursion. - * - * @param children the node of the trie to search under. - * @param word the word to search for. Only read [offset..length] so there may be trailing chars - * @param offset the index in {@code word} this recursion should operate on. - * @param length the length of the input word. - * @return Returns the terminal node of the word if the word exists - */ - private Node searchNode(final NodeArray children, final CharSequence word, final int offset, - final int length) { - final int count = children.mLength; - final char currentChar = word.charAt(offset); - for (int j = 0; j < count; j++) { - final Node node = children.mData[j]; - if (node.mCode == currentChar) { - if (offset == length - 1) { - if (node.mTerminal) { - return node; - } - } else { - if (node.mChildren != null) { - Node returnNode = searchNode(node.mChildren, word, offset + 1, length); - if (returnNode != null) return returnNode; - } - } - } - } - return null; - } - - public void clearDictionary() { - mRoots = new NodeArray(); - } - - private static char toLowerCase(final char c) { - char baseChar = c; - if (c < BASE_CHARS.length) { - baseChar = BASE_CHARS[c]; - } - if (baseChar >= 'A' && baseChar <= 'Z') { - return (char)(baseChar | 32); - } else if (baseChar > 127) { - return Character.toLowerCase(baseChar); - } - return baseChar; - } - - /** - * Table mapping most combined Latin, Greek, and Cyrillic characters - * to their base characters. If c is in range, BASE_CHARS[c] == c - * if c is not a combined character, or the base character if it - * is combined. - * - * cf. native/jni/src/utils/char_utils.cpp - */ - private static final char BASE_CHARS[] = { - /* U+0000 */ 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, - /* U+0008 */ 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F, - /* U+0010 */ 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017, - /* U+0018 */ 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F, - /* U+0020 */ 0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027, - /* U+0028 */ 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F, - /* U+0030 */ 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, - /* U+0038 */ 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F, - /* U+0040 */ 0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, - /* U+0048 */ 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, - /* U+0050 */ 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, - /* U+0058 */ 0x0058, 0x0059, 0x005A, 0x005B, 0x005C, 0x005D, 0x005E, 0x005F, - /* U+0060 */ 0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, - /* U+0068 */ 0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, - /* U+0070 */ 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, - /* U+0078 */ 0x0078, 0x0079, 0x007A, 0x007B, 0x007C, 0x007D, 0x007E, 0x007F, - /* U+0080 */ 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, - /* U+0088 */ 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, 0x008F, - /* U+0090 */ 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, - /* U+0098 */ 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F, - /* U+00A0 */ 0x0020, 0x00A1, 0x00A2, 0x00A3, 0x00A4, 0x00A5, 0x00A6, 0x00A7, - /* U+00A8 */ 0x0020, 0x00A9, 0x0061, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x0020, - /* U+00B0 */ 0x00B0, 0x00B1, 0x0032, 0x0033, 0x0020, 0x03BC, 0x00B6, 0x00B7, - /* U+00B8 */ 0x0020, 0x0031, 0x006F, 0x00BB, 0x0031, 0x0031, 0x0033, 0x00BF, - /* U+00C0 */ 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x00C6, 0x0043, - /* U+00C8 */ 0x0045, 0x0045, 0x0045, 0x0045, 0x0049, 0x0049, 0x0049, 0x0049, - /* U+00D0 */ 0x00D0, 0x004E, 0x004F, 0x004F, 0x004F, 0x004F, 0x004F, 0x00D7, - /* U+00D8 */ 0x004F, 0x0055, 0x0055, 0x0055, 0x0055, 0x0059, 0x00DE, 0x0073, - // U+00D8: Manually changed from 00D8 to 004F - // TODO: Check if it's really acceptable to consider Ø a diacritical variant of O - // U+00DF: Manually changed from 00DF to 0073 - /* U+00E0 */ 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x00E6, 0x0063, - /* U+00E8 */ 0x0065, 0x0065, 0x0065, 0x0065, 0x0069, 0x0069, 0x0069, 0x0069, - /* U+00F0 */ 0x00F0, 0x006E, 0x006F, 0x006F, 0x006F, 0x006F, 0x006F, 0x00F7, - /* U+00F8 */ 0x006F, 0x0075, 0x0075, 0x0075, 0x0075, 0x0079, 0x00FE, 0x0079, - // U+00F8: Manually changed from 00F8 to 006F - // TODO: Check if it's really acceptable to consider ø a diacritical variant of o - /* U+0100 */ 0x0041, 0x0061, 0x0041, 0x0061, 0x0041, 0x0061, 0x0043, 0x0063, - /* U+0108 */ 0x0043, 0x0063, 0x0043, 0x0063, 0x0043, 0x0063, 0x0044, 0x0064, - /* U+0110 */ 0x0110, 0x0111, 0x0045, 0x0065, 0x0045, 0x0065, 0x0045, 0x0065, - /* U+0118 */ 0x0045, 0x0065, 0x0045, 0x0065, 0x0047, 0x0067, 0x0047, 0x0067, - /* U+0120 */ 0x0047, 0x0067, 0x0047, 0x0067, 0x0048, 0x0068, 0x0126, 0x0127, - /* U+0128 */ 0x0049, 0x0069, 0x0049, 0x0069, 0x0049, 0x0069, 0x0049, 0x0069, - /* U+0130 */ 0x0049, 0x0131, 0x0049, 0x0069, 0x004A, 0x006A, 0x004B, 0x006B, - /* U+0138 */ 0x0138, 0x004C, 0x006C, 0x004C, 0x006C, 0x004C, 0x006C, 0x004C, - /* U+0140 */ 0x006C, 0x004C, 0x006C, 0x004E, 0x006E, 0x004E, 0x006E, 0x004E, - // U+0141: Manually changed from 0141 to 004C - // U+0142: Manually changed from 0142 to 006C - /* U+0148 */ 0x006E, 0x02BC, 0x014A, 0x014B, 0x004F, 0x006F, 0x004F, 0x006F, - /* U+0150 */ 0x004F, 0x006F, 0x0152, 0x0153, 0x0052, 0x0072, 0x0052, 0x0072, - /* U+0158 */ 0x0052, 0x0072, 0x0053, 0x0073, 0x0053, 0x0073, 0x0053, 0x0073, - /* U+0160 */ 0x0053, 0x0073, 0x0054, 0x0074, 0x0054, 0x0074, 0x0166, 0x0167, - /* U+0168 */ 0x0055, 0x0075, 0x0055, 0x0075, 0x0055, 0x0075, 0x0055, 0x0075, - /* U+0170 */ 0x0055, 0x0075, 0x0055, 0x0075, 0x0057, 0x0077, 0x0059, 0x0079, - /* U+0178 */ 0x0059, 0x005A, 0x007A, 0x005A, 0x007A, 0x005A, 0x007A, 0x0073, - /* U+0180 */ 0x0180, 0x0181, 0x0182, 0x0183, 0x0184, 0x0185, 0x0186, 0x0187, - /* U+0188 */ 0x0188, 0x0189, 0x018A, 0x018B, 0x018C, 0x018D, 0x018E, 0x018F, - /* U+0190 */ 0x0190, 0x0191, 0x0192, 0x0193, 0x0194, 0x0195, 0x0196, 0x0197, - /* U+0198 */ 0x0198, 0x0199, 0x019A, 0x019B, 0x019C, 0x019D, 0x019E, 0x019F, - /* U+01A0 */ 0x004F, 0x006F, 0x01A2, 0x01A3, 0x01A4, 0x01A5, 0x01A6, 0x01A7, - /* U+01A8 */ 0x01A8, 0x01A9, 0x01AA, 0x01AB, 0x01AC, 0x01AD, 0x01AE, 0x0055, - /* U+01B0 */ 0x0075, 0x01B1, 0x01B2, 0x01B3, 0x01B4, 0x01B5, 0x01B6, 0x01B7, - /* U+01B8 */ 0x01B8, 0x01B9, 0x01BA, 0x01BB, 0x01BC, 0x01BD, 0x01BE, 0x01BF, - /* U+01C0 */ 0x01C0, 0x01C1, 0x01C2, 0x01C3, 0x0044, 0x0044, 0x0064, 0x004C, - /* U+01C8 */ 0x004C, 0x006C, 0x004E, 0x004E, 0x006E, 0x0041, 0x0061, 0x0049, - /* U+01D0 */ 0x0069, 0x004F, 0x006F, 0x0055, 0x0075, 0x0055, 0x0075, 0x0055, - // U+01D5: Manually changed from 00DC to 0055 - // U+01D6: Manually changed from 00FC to 0075 - // U+01D7: Manually changed from 00DC to 0055 - /* U+01D8 */ 0x0075, 0x0055, 0x0075, 0x0055, 0x0075, 0x01DD, 0x0041, 0x0061, - // U+01D8: Manually changed from 00FC to 0075 - // U+01D9: Manually changed from 00DC to 0055 - // U+01DA: Manually changed from 00FC to 0075 - // U+01DB: Manually changed from 00DC to 0055 - // U+01DC: Manually changed from 00FC to 0075 - // U+01DE: Manually changed from 00C4 to 0041 - // U+01DF: Manually changed from 00E4 to 0061 - /* U+01E0 */ 0x0041, 0x0061, 0x00C6, 0x00E6, 0x01E4, 0x01E5, 0x0047, 0x0067, - // U+01E0: Manually changed from 0226 to 0041 - // U+01E1: Manually changed from 0227 to 0061 - /* U+01E8 */ 0x004B, 0x006B, 0x004F, 0x006F, 0x004F, 0x006F, 0x01B7, 0x0292, - // U+01EC: Manually changed from 01EA to 004F - // U+01ED: Manually changed from 01EB to 006F - /* U+01F0 */ 0x006A, 0x0044, 0x0044, 0x0064, 0x0047, 0x0067, 0x01F6, 0x01F7, - /* U+01F8 */ 0x004E, 0x006E, 0x0041, 0x0061, 0x00C6, 0x00E6, 0x004F, 0x006F, - // U+01FA: Manually changed from 00C5 to 0041 - // U+01FB: Manually changed from 00E5 to 0061 - // U+01FE: Manually changed from 00D8 to 004F - // TODO: Check if it's really acceptable to consider Ø a diacritical variant of O - // U+01FF: Manually changed from 00F8 to 006F - // TODO: Check if it's really acceptable to consider ø a diacritical variant of o - /* U+0200 */ 0x0041, 0x0061, 0x0041, 0x0061, 0x0045, 0x0065, 0x0045, 0x0065, - /* U+0208 */ 0x0049, 0x0069, 0x0049, 0x0069, 0x004F, 0x006F, 0x004F, 0x006F, - /* U+0210 */ 0x0052, 0x0072, 0x0052, 0x0072, 0x0055, 0x0075, 0x0055, 0x0075, - /* U+0218 */ 0x0053, 0x0073, 0x0054, 0x0074, 0x021C, 0x021D, 0x0048, 0x0068, - /* U+0220 */ 0x0220, 0x0221, 0x0222, 0x0223, 0x0224, 0x0225, 0x0041, 0x0061, - /* U+0228 */ 0x0045, 0x0065, 0x004F, 0x006F, 0x004F, 0x006F, 0x004F, 0x006F, - // U+022A: Manually changed from 00D6 to 004F - // U+022B: Manually changed from 00F6 to 006F - // U+022C: Manually changed from 00D5 to 004F - // U+022D: Manually changed from 00F5 to 006F - /* U+0230 */ 0x004F, 0x006F, 0x0059, 0x0079, 0x0234, 0x0235, 0x0236, 0x0237, - // U+0230: Manually changed from 022E to 004F - // U+0231: Manually changed from 022F to 006F - /* U+0238 */ 0x0238, 0x0239, 0x023A, 0x023B, 0x023C, 0x023D, 0x023E, 0x023F, - /* U+0240 */ 0x0240, 0x0241, 0x0242, 0x0243, 0x0244, 0x0245, 0x0246, 0x0247, - /* U+0248 */ 0x0248, 0x0249, 0x024A, 0x024B, 0x024C, 0x024D, 0x024E, 0x024F, - /* U+0250 */ 0x0250, 0x0251, 0x0252, 0x0253, 0x0254, 0x0255, 0x0256, 0x0257, - /* U+0258 */ 0x0258, 0x0259, 0x025A, 0x025B, 0x025C, 0x025D, 0x025E, 0x025F, - /* U+0260 */ 0x0260, 0x0261, 0x0262, 0x0263, 0x0264, 0x0265, 0x0266, 0x0267, - /* U+0268 */ 0x0268, 0x0269, 0x026A, 0x026B, 0x026C, 0x026D, 0x026E, 0x026F, - /* U+0270 */ 0x0270, 0x0271, 0x0272, 0x0273, 0x0274, 0x0275, 0x0276, 0x0277, - /* U+0278 */ 0x0278, 0x0279, 0x027A, 0x027B, 0x027C, 0x027D, 0x027E, 0x027F, - /* U+0280 */ 0x0280, 0x0281, 0x0282, 0x0283, 0x0284, 0x0285, 0x0286, 0x0287, - /* U+0288 */ 0x0288, 0x0289, 0x028A, 0x028B, 0x028C, 0x028D, 0x028E, 0x028F, - /* U+0290 */ 0x0290, 0x0291, 0x0292, 0x0293, 0x0294, 0x0295, 0x0296, 0x0297, - /* U+0298 */ 0x0298, 0x0299, 0x029A, 0x029B, 0x029C, 0x029D, 0x029E, 0x029F, - /* U+02A0 */ 0x02A0, 0x02A1, 0x02A2, 0x02A3, 0x02A4, 0x02A5, 0x02A6, 0x02A7, - /* U+02A8 */ 0x02A8, 0x02A9, 0x02AA, 0x02AB, 0x02AC, 0x02AD, 0x02AE, 0x02AF, - /* U+02B0 */ 0x0068, 0x0266, 0x006A, 0x0072, 0x0279, 0x027B, 0x0281, 0x0077, - /* U+02B8 */ 0x0079, 0x02B9, 0x02BA, 0x02BB, 0x02BC, 0x02BD, 0x02BE, 0x02BF, - /* U+02C0 */ 0x02C0, 0x02C1, 0x02C2, 0x02C3, 0x02C4, 0x02C5, 0x02C6, 0x02C7, - /* U+02C8 */ 0x02C8, 0x02C9, 0x02CA, 0x02CB, 0x02CC, 0x02CD, 0x02CE, 0x02CF, - /* U+02D0 */ 0x02D0, 0x02D1, 0x02D2, 0x02D3, 0x02D4, 0x02D5, 0x02D6, 0x02D7, - /* U+02D8 */ 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x02DE, 0x02DF, - /* U+02E0 */ 0x0263, 0x006C, 0x0073, 0x0078, 0x0295, 0x02E5, 0x02E6, 0x02E7, - /* U+02E8 */ 0x02E8, 0x02E9, 0x02EA, 0x02EB, 0x02EC, 0x02ED, 0x02EE, 0x02EF, - /* U+02F0 */ 0x02F0, 0x02F1, 0x02F2, 0x02F3, 0x02F4, 0x02F5, 0x02F6, 0x02F7, - /* U+02F8 */ 0x02F8, 0x02F9, 0x02FA, 0x02FB, 0x02FC, 0x02FD, 0x02FE, 0x02FF, - /* U+0300 */ 0x0300, 0x0301, 0x0302, 0x0303, 0x0304, 0x0305, 0x0306, 0x0307, - /* U+0308 */ 0x0308, 0x0309, 0x030A, 0x030B, 0x030C, 0x030D, 0x030E, 0x030F, - /* U+0310 */ 0x0310, 0x0311, 0x0312, 0x0313, 0x0314, 0x0315, 0x0316, 0x0317, - /* U+0318 */ 0x0318, 0x0319, 0x031A, 0x031B, 0x031C, 0x031D, 0x031E, 0x031F, - /* U+0320 */ 0x0320, 0x0321, 0x0322, 0x0323, 0x0324, 0x0325, 0x0326, 0x0327, - /* U+0328 */ 0x0328, 0x0329, 0x032A, 0x032B, 0x032C, 0x032D, 0x032E, 0x032F, - /* U+0330 */ 0x0330, 0x0331, 0x0332, 0x0333, 0x0334, 0x0335, 0x0336, 0x0337, - /* U+0338 */ 0x0338, 0x0339, 0x033A, 0x033B, 0x033C, 0x033D, 0x033E, 0x033F, - /* U+0340 */ 0x0300, 0x0301, 0x0342, 0x0313, 0x0308, 0x0345, 0x0346, 0x0347, - /* U+0348 */ 0x0348, 0x0349, 0x034A, 0x034B, 0x034C, 0x034D, 0x034E, 0x034F, - /* U+0350 */ 0x0350, 0x0351, 0x0352, 0x0353, 0x0354, 0x0355, 0x0356, 0x0357, - /* U+0358 */ 0x0358, 0x0359, 0x035A, 0x035B, 0x035C, 0x035D, 0x035E, 0x035F, - /* U+0360 */ 0x0360, 0x0361, 0x0362, 0x0363, 0x0364, 0x0365, 0x0366, 0x0367, - /* U+0368 */ 0x0368, 0x0369, 0x036A, 0x036B, 0x036C, 0x036D, 0x036E, 0x036F, - /* U+0370 */ 0x0370, 0x0371, 0x0372, 0x0373, 0x02B9, 0x0375, 0x0376, 0x0377, - /* U+0378 */ 0x0378, 0x0379, 0x0020, 0x037B, 0x037C, 0x037D, 0x003B, 0x037F, - /* U+0380 */ 0x0380, 0x0381, 0x0382, 0x0383, 0x0020, 0x00A8, 0x0391, 0x00B7, - /* U+0388 */ 0x0395, 0x0397, 0x0399, 0x038B, 0x039F, 0x038D, 0x03A5, 0x03A9, - /* U+0390 */ 0x03CA, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397, - /* U+0398 */ 0x0398, 0x0399, 0x039A, 0x039B, 0x039C, 0x039D, 0x039E, 0x039F, - /* U+03A0 */ 0x03A0, 0x03A1, 0x03A2, 0x03A3, 0x03A4, 0x03A5, 0x03A6, 0x03A7, - /* U+03A8 */ 0x03A8, 0x03A9, 0x0399, 0x03A5, 0x03B1, 0x03B5, 0x03B7, 0x03B9, - /* U+03B0 */ 0x03CB, 0x03B1, 0x03B2, 0x03B3, 0x03B4, 0x03B5, 0x03B6, 0x03B7, - /* U+03B8 */ 0x03B8, 0x03B9, 0x03BA, 0x03BB, 0x03BC, 0x03BD, 0x03BE, 0x03BF, - /* U+03C0 */ 0x03C0, 0x03C1, 0x03C2, 0x03C3, 0x03C4, 0x03C5, 0x03C6, 0x03C7, - /* U+03C8 */ 0x03C8, 0x03C9, 0x03B9, 0x03C5, 0x03BF, 0x03C5, 0x03C9, 0x03CF, - /* U+03D0 */ 0x03B2, 0x03B8, 0x03A5, 0x03D2, 0x03D2, 0x03C6, 0x03C0, 0x03D7, - /* U+03D8 */ 0x03D8, 0x03D9, 0x03DA, 0x03DB, 0x03DC, 0x03DD, 0x03DE, 0x03DF, - /* U+03E0 */ 0x03E0, 0x03E1, 0x03E2, 0x03E3, 0x03E4, 0x03E5, 0x03E6, 0x03E7, - /* U+03E8 */ 0x03E8, 0x03E9, 0x03EA, 0x03EB, 0x03EC, 0x03ED, 0x03EE, 0x03EF, - /* U+03F0 */ 0x03BA, 0x03C1, 0x03C2, 0x03F3, 0x0398, 0x03B5, 0x03F6, 0x03F7, - /* U+03F8 */ 0x03F8, 0x03A3, 0x03FA, 0x03FB, 0x03FC, 0x03FD, 0x03FE, 0x03FF, - /* U+0400 */ 0x0415, 0x0415, 0x0402, 0x0413, 0x0404, 0x0405, 0x0406, 0x0406, - /* U+0408 */ 0x0408, 0x0409, 0x040A, 0x040B, 0x041A, 0x0418, 0x0423, 0x040F, - /* U+0410 */ 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417, - /* U+0418 */ 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F, - // U+0419: Manually changed from 0418 to 0419 - /* U+0420 */ 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, - /* U+0428 */ 0x0428, 0x0429, 0x042C, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F, - // U+042A: Manually changed from 042A to 042C - /* U+0430 */ 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437, - /* U+0438 */ 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, 0x043F, - // U+0439: Manually changed from 0438 to 0439 - /* U+0440 */ 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447, - /* U+0448 */ 0x0448, 0x0449, 0x044C, 0x044B, 0x044C, 0x044D, 0x044E, 0x044F, - // U+044A: Manually changed from 044A to 044C - /* U+0450 */ 0x0435, 0x0435, 0x0452, 0x0433, 0x0454, 0x0455, 0x0456, 0x0456, - /* U+0458 */ 0x0458, 0x0459, 0x045A, 0x045B, 0x043A, 0x0438, 0x0443, 0x045F, - /* U+0460 */ 0x0460, 0x0461, 0x0462, 0x0463, 0x0464, 0x0465, 0x0466, 0x0467, - /* U+0468 */ 0x0468, 0x0469, 0x046A, 0x046B, 0x046C, 0x046D, 0x046E, 0x046F, - /* U+0470 */ 0x0470, 0x0471, 0x0472, 0x0473, 0x0474, 0x0475, 0x0474, 0x0475, - /* U+0478 */ 0x0478, 0x0479, 0x047A, 0x047B, 0x047C, 0x047D, 0x047E, 0x047F, - /* U+0480 */ 0x0480, 0x0481, 0x0482, 0x0483, 0x0484, 0x0485, 0x0486, 0x0487, - /* U+0488 */ 0x0488, 0x0489, 0x048A, 0x048B, 0x048C, 0x048D, 0x048E, 0x048F, - /* U+0490 */ 0x0490, 0x0491, 0x0492, 0x0493, 0x0494, 0x0495, 0x0496, 0x0497, - /* U+0498 */ 0x0498, 0x0499, 0x049A, 0x049B, 0x049C, 0x049D, 0x049E, 0x049F, - /* U+04A0 */ 0x04A0, 0x04A1, 0x04A2, 0x04A3, 0x04A4, 0x04A5, 0x04A6, 0x04A7, - /* U+04A8 */ 0x04A8, 0x04A9, 0x04AA, 0x04AB, 0x04AC, 0x04AD, 0x04AE, 0x04AF, - /* U+04B0 */ 0x04B0, 0x04B1, 0x04B2, 0x04B3, 0x04B4, 0x04B5, 0x04B6, 0x04B7, - /* U+04B8 */ 0x04B8, 0x04B9, 0x04BA, 0x04BB, 0x04BC, 0x04BD, 0x04BE, 0x04BF, - /* U+04C0 */ 0x04C0, 0x0416, 0x0436, 0x04C3, 0x04C4, 0x04C5, 0x04C6, 0x04C7, - /* U+04C8 */ 0x04C8, 0x04C9, 0x04CA, 0x04CB, 0x04CC, 0x04CD, 0x04CE, 0x04CF, - /* U+04D0 */ 0x0410, 0x0430, 0x0410, 0x0430, 0x04D4, 0x04D5, 0x0415, 0x0435, - /* U+04D8 */ 0x04D8, 0x04D9, 0x04D8, 0x04D9, 0x0416, 0x0436, 0x0417, 0x0437, - /* U+04E0 */ 0x04E0, 0x04E1, 0x0418, 0x0438, 0x0418, 0x0438, 0x041E, 0x043E, - /* U+04E8 */ 0x04E8, 0x04E9, 0x04E8, 0x04E9, 0x042D, 0x044D, 0x0423, 0x0443, - /* U+04F0 */ 0x0423, 0x0443, 0x0423, 0x0443, 0x0427, 0x0447, 0x04F6, 0x04F7, - /* U+04F8 */ 0x042B, 0x044B, 0x04FA, 0x04FB, 0x04FC, 0x04FD, 0x04FE, 0x04FF, - }; -} diff --git a/java/src/com/android/inputmethod/latin/ImportantNoticeDialog.java b/java/src/com/android/inputmethod/latin/ImportantNoticeDialog.java new file mode 100644 index 000000000..567087c81 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/ImportantNoticeDialog.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; + +import com.android.inputmethod.latin.utils.DialogUtils; +import com.android.inputmethod.latin.utils.ImportantNoticeUtils; + +/** + * The dialog box that shows the important notice contents. + */ +public final class ImportantNoticeDialog extends AlertDialog implements OnClickListener { + public interface ImportantNoticeDialogListener { + public void onUserAcknowledgmentOfImportantNoticeDialog(final int nextVersion); + public void onClickSettingsOfImportantNoticeDialog(final int nextVersion); + } + + private final ImportantNoticeDialogListener mListener; + private final int mNextImportantNoticeVersion; + + public ImportantNoticeDialog( + final Context context, final ImportantNoticeDialogListener listener) { + super(DialogUtils.getPlatformDialogThemeContext(context)); + mListener = listener; + mNextImportantNoticeVersion = ImportantNoticeUtils.getNextImportantNoticeVersion(context); + setMessage(ImportantNoticeUtils.getNextImportantNoticeContents(context)); + // Create buttons and set listeners. + setButton(BUTTON_POSITIVE, context.getString(android.R.string.ok), this); + if (shouldHaveSettingsButton()) { + setButton(BUTTON_NEGATIVE, context.getString(R.string.go_to_settings), this); + } + // This dialog is cancelable by pressing back key. See {@link #onBackPress()}. + setCancelable(true /* cancelable */); + setCanceledOnTouchOutside(false /* cancelable */); + } + + private boolean shouldHaveSettingsButton() { + return mNextImportantNoticeVersion + == ImportantNoticeUtils.VERSION_TO_ENABLE_PERSONALIZED_SUGGESTIONS; + } + + private void userAcknowledged() { + ImportantNoticeUtils.updateLastImportantNoticeVersion(getContext()); + mListener.onUserAcknowledgmentOfImportantNoticeDialog(mNextImportantNoticeVersion); + } + + @Override + public void onClick(final DialogInterface dialog, final int which) { + if (shouldHaveSettingsButton() && which == BUTTON_NEGATIVE) { + mListener.onClickSettingsOfImportantNoticeDialog(mNextImportantNoticeVersion); + } + userAcknowledged(); + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + userAcknowledged(); + } +} diff --git a/java/src/com/android/inputmethod/latin/InputAttributes.java b/java/src/com/android/inputmethod/latin/InputAttributes.java index 8caf6f17f..ebe436128 100644 --- a/java/src/com/android/inputmethod/latin/InputAttributes.java +++ b/java/src/com/android/inputmethod/latin/InputAttributes.java @@ -16,6 +16,9 @@ package com.android.inputmethod.latin; +import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE; +import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE_COMPAT; + import android.text.InputType; import android.util.Log; import android.view.inputmethod.EditorInfo; @@ -23,22 +26,36 @@ import android.view.inputmethod.EditorInfo; import com.android.inputmethod.latin.utils.InputTypeUtils; import com.android.inputmethod.latin.utils.StringUtils; +import java.util.ArrayList; +import java.util.Arrays; + /** * Class to hold attributes of the input field. */ public final class InputAttributes { private final String TAG = InputAttributes.class.getSimpleName(); + final public String mTargetApplicationPackageName; final public boolean mInputTypeNoAutoCorrect; - final public boolean mIsSettingsSuggestionStripOn; + final public boolean mIsPasswordField; + final public boolean mShouldShowSuggestions; final public boolean mApplicationSpecifiedCompletionOn; final public boolean mShouldInsertSpacesAutomatically; + final public boolean mShouldShowVoiceInputKey; final private int mInputType; + final private EditorInfo mEditorInfo; + final private String mPackageNameForPrivateImeOptions; - public InputAttributes(final EditorInfo editorInfo, final boolean isFullscreenMode) { + public InputAttributes(final EditorInfo editorInfo, final boolean isFullscreenMode, + final String packageNameForPrivateImeOptions) { + mEditorInfo = editorInfo; + mPackageNameForPrivateImeOptions = packageNameForPrivateImeOptions; + mTargetApplicationPackageName = null != editorInfo ? editorInfo.packageName : null; final int inputType = null != editorInfo ? editorInfo.inputType : 0; final int inputClass = inputType & InputType.TYPE_MASK_CLASS; mInputType = inputType; + mIsPasswordField = InputTypeUtils.isPasswordInputType(inputType) + || InputTypeUtils.isVisiblePasswordInputType(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 @@ -52,55 +69,54 @@ public final class InputAttributes { } else if (inputClass == 0) { // TODO: is this check still necessary? Log.w(TAG, String.format("Unexpected input class: inputType=0x%08x" - + " imeOptions=0x%08x", - inputType, editorInfo.imeOptions)); + + " imeOptions=0x%08x", inputType, editorInfo.imeOptions)); } - mIsSettingsSuggestionStripOn = false; + mShouldShowSuggestions = false; mInputTypeNoAutoCorrect = false; mApplicationSpecifiedCompletionOn = false; mShouldInsertSpacesAutomatically = false; - } else { - final int variation = inputType & InputType.TYPE_MASK_VARIATION; - final boolean flagNoSuggestions = - 0 != (inputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); - final boolean flagMultiLine = - 0 != (inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE); - final boolean flagAutoCorrect = - 0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT); - final boolean flagAutoComplete = - 0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE); - - // TODO: Have a helper method in InputTypeUtils - // Make sure that passwords are not displayed in {@link SuggestionStripView}. - if (InputTypeUtils.isPasswordInputType(inputType) - || InputTypeUtils.isVisiblePasswordInputType(inputType) - || InputTypeUtils.isEmailVariation(variation) - || InputType.TYPE_TEXT_VARIATION_URI == variation - || InputType.TYPE_TEXT_VARIATION_FILTER == variation - || flagNoSuggestions - || flagAutoComplete) { - mIsSettingsSuggestionStripOn = false; - } else { - mIsSettingsSuggestionStripOn = true; - } + mShouldShowVoiceInputKey = false; + return; + } + // inputClass == InputType.TYPE_CLASS_TEXT + final int variation = inputType & InputType.TYPE_MASK_VARIATION; + final boolean flagNoSuggestions = + 0 != (inputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + final boolean flagMultiLine = + 0 != (inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE); + final boolean flagAutoCorrect = + 0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT); + final boolean flagAutoComplete = + 0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE); - mShouldInsertSpacesAutomatically = InputTypeUtils.isAutoSpaceFriendlyType(inputType); - - // If it's a browser edit field and auto correct is not ON explicitly, then - // disable auto correction, but keep suggestions on. - // If NO_SUGGESTIONS is set, don't do prediction. - // If it's not multiline and the autoCorrect flag is not set, then don't correct - if ((variation == InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT - && !flagAutoCorrect) - || flagNoSuggestions - || (!flagAutoCorrect && !flagMultiLine)) { - mInputTypeNoAutoCorrect = true; - } else { - mInputTypeNoAutoCorrect = false; - } + // TODO: Have a helper method in InputTypeUtils + // Make sure that passwords are not displayed in {@link SuggestionStripView}. + final boolean shouldSuppressSuggestions = mIsPasswordField + || InputTypeUtils.isEmailVariation(variation) + || InputType.TYPE_TEXT_VARIATION_URI == variation + || InputType.TYPE_TEXT_VARIATION_FILTER == variation + || flagNoSuggestions + || flagAutoComplete; + mShouldShowSuggestions = !shouldSuppressSuggestions; - mApplicationSpecifiedCompletionOn = flagAutoComplete && isFullscreenMode; - } + mShouldInsertSpacesAutomatically = InputTypeUtils.isAutoSpaceFriendlyType(inputType); + + final boolean noMicrophone = mIsPasswordField + || InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS == variation + || InputType.TYPE_TEXT_VARIATION_URI == variation + || hasNoMicrophoneKeyOption(); + mShouldShowVoiceInputKey = !noMicrophone; + + // If it's a browser edit field and auto correct is not ON explicitly, then + // disable auto correction, but keep suggestions on. + // If NO_SUGGESTIONS is set, don't do prediction. + // If it's not multiline and the autoCorrect flag is not set, then don't correct + mInputTypeNoAutoCorrect = + (variation == InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT && !flagAutoCorrect) + || flagNoSuggestions + || (!flagAutoCorrect && !flagMultiLine); + + mApplicationSpecifiedCompletionOn = flagAutoComplete && isFullscreenMode; } public boolean isTypeNull() { @@ -111,101 +127,155 @@ public final class InputAttributes { return editorInfo.inputType == mInputType; } + private boolean hasNoMicrophoneKeyOption() { + @SuppressWarnings("deprecation") + final boolean deprecatedNoMicrophone = InputAttributes.inPrivateImeOptions( + null, NO_MICROPHONE_COMPAT, mEditorInfo); + final boolean noMicrophone = InputAttributes.inPrivateImeOptions( + mPackageNameForPrivateImeOptions, NO_MICROPHONE, mEditorInfo); + return noMicrophone || deprecatedNoMicrophone; + } + @SuppressWarnings("unused") private void dumpFlags(final int inputType) { - Log.i(TAG, "Input class:"); final int inputClass = inputType & InputType.TYPE_MASK_CLASS; - if (inputClass == InputType.TYPE_CLASS_TEXT) - Log.i(TAG, " TYPE_CLASS_TEXT"); - if (inputClass == InputType.TYPE_CLASS_PHONE) - Log.i(TAG, " TYPE_CLASS_PHONE"); - if (inputClass == InputType.TYPE_CLASS_NUMBER) - Log.i(TAG, " TYPE_CLASS_NUMBER"); - if (inputClass == InputType.TYPE_CLASS_DATETIME) - Log.i(TAG, " TYPE_CLASS_DATETIME"); - Log.i(TAG, "Variation:"); - switch (InputType.TYPE_MASK_VARIATION & inputType) { - case InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS: - Log.i(TAG, " TYPE_TEXT_VARIATION_EMAIL_ADDRESS"); - break; - case InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT: - Log.i(TAG, " TYPE_TEXT_VARIATION_EMAIL_SUBJECT"); - break; - case InputType.TYPE_TEXT_VARIATION_FILTER: - Log.i(TAG, " TYPE_TEXT_VARIATION_FILTER"); - break; - case InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE: - Log.i(TAG, " TYPE_TEXT_VARIATION_LONG_MESSAGE"); - break; - case InputType.TYPE_TEXT_VARIATION_NORMAL: - Log.i(TAG, " TYPE_TEXT_VARIATION_NORMAL"); - break; - case InputType.TYPE_TEXT_VARIATION_PASSWORD: - Log.i(TAG, " TYPE_TEXT_VARIATION_PASSWORD"); - break; - case InputType.TYPE_TEXT_VARIATION_PERSON_NAME: - Log.i(TAG, " TYPE_TEXT_VARIATION_PERSON_NAME"); - break; - case InputType.TYPE_TEXT_VARIATION_PHONETIC: - Log.i(TAG, " TYPE_TEXT_VARIATION_PHONETIC"); - break; - case InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS: - Log.i(TAG, " TYPE_TEXT_VARIATION_POSTAL_ADDRESS"); - break; - case InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE: - Log.i(TAG, " TYPE_TEXT_VARIATION_SHORT_MESSAGE"); - break; - case InputType.TYPE_TEXT_VARIATION_URI: - Log.i(TAG, " TYPE_TEXT_VARIATION_URI"); - break; - case InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD: - Log.i(TAG, " TYPE_TEXT_VARIATION_VISIBLE_PASSWORD"); - break; - case InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT: - Log.i(TAG, " TYPE_TEXT_VARIATION_WEB_EDIT_TEXT"); - break; - case InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS: - Log.i(TAG, " TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS"); - break; - case InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD: - Log.i(TAG, " TYPE_TEXT_VARIATION_WEB_PASSWORD"); - break; - default: - Log.i(TAG, " Unknown variation"); - break; + final String inputClassString = toInputClassString(inputClass); + final String variationString = toVariationString( + inputClass, inputType & InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS); + final String flagsString = toFlagsString(inputType & InputType.TYPE_MASK_FLAGS); + Log.i(TAG, "Input class: " + inputClassString); + Log.i(TAG, "Variation: " + variationString); + Log.i(TAG, "Flags: " + flagsString); + } + + private static String toInputClassString(final int inputClass) { + switch (inputClass) { + case InputType.TYPE_CLASS_TEXT: + return "TYPE_CLASS_TEXT"; + case InputType.TYPE_CLASS_PHONE: + return "TYPE_CLASS_PHONE"; + case InputType.TYPE_CLASS_NUMBER: + return "TYPE_CLASS_NUMBER"; + case InputType.TYPE_CLASS_DATETIME: + return "TYPE_CLASS_DATETIME"; + default: + return String.format("unknownInputClass<0x%08x>", inputClass); } - Log.i(TAG, "Flags:"); - if (0 != (inputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS)) - Log.i(TAG, " TYPE_TEXT_FLAG_NO_SUGGESTIONS"); - if (0 != (inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE)) - Log.i(TAG, " TYPE_TEXT_FLAG_MULTI_LINE"); - if (0 != (inputType & InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE)) - Log.i(TAG, " TYPE_TEXT_FLAG_IME_MULTI_LINE"); - if (0 != (inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS)) - Log.i(TAG, " TYPE_TEXT_FLAG_CAP_WORDS"); - if (0 != (inputType & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)) - Log.i(TAG, " TYPE_TEXT_FLAG_CAP_SENTENCES"); - if (0 != (inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS)) - Log.i(TAG, " TYPE_TEXT_FLAG_CAP_CHARACTERS"); - if (0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT)) - Log.i(TAG, " TYPE_TEXT_FLAG_AUTO_CORRECT"); - if (0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE)) - Log.i(TAG, " TYPE_TEXT_FLAG_AUTO_COMPLETE"); + } + + private static String toVariationString(final int inputClass, final int variation) { + switch (inputClass) { + case InputType.TYPE_CLASS_TEXT: + return toTextVariationString(variation); + case InputType.TYPE_CLASS_NUMBER: + return toNumberVariationString(variation); + case InputType.TYPE_CLASS_DATETIME: + return toDatetimeVariationString(variation); + default: + return ""; + } + } + + private static String toTextVariationString(final int variation) { + switch (variation) { + case InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS: + return " TYPE_TEXT_VARIATION_EMAIL_ADDRESS"; + case InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT: + return "TYPE_TEXT_VARIATION_EMAIL_SUBJECT"; + case InputType.TYPE_TEXT_VARIATION_FILTER: + return "TYPE_TEXT_VARIATION_FILTER"; + case InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE: + return "TYPE_TEXT_VARIATION_LONG_MESSAGE"; + case InputType.TYPE_TEXT_VARIATION_NORMAL: + return "TYPE_TEXT_VARIATION_NORMAL"; + case InputType.TYPE_TEXT_VARIATION_PASSWORD: + return "TYPE_TEXT_VARIATION_PASSWORD"; + case InputType.TYPE_TEXT_VARIATION_PERSON_NAME: + return "TYPE_TEXT_VARIATION_PERSON_NAME"; + case InputType.TYPE_TEXT_VARIATION_PHONETIC: + return "TYPE_TEXT_VARIATION_PHONETIC"; + case InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS: + return "TYPE_TEXT_VARIATION_POSTAL_ADDRESS"; + case InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE: + return "TYPE_TEXT_VARIATION_SHORT_MESSAGE"; + case InputType.TYPE_TEXT_VARIATION_URI: + return "TYPE_TEXT_VARIATION_URI"; + case InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD: + return "TYPE_TEXT_VARIATION_VISIBLE_PASSWORD"; + case InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT: + return "TYPE_TEXT_VARIATION_WEB_EDIT_TEXT"; + case InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS: + return "TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS"; + case InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD: + return "TYPE_TEXT_VARIATION_WEB_PASSWORD"; + default: + return String.format("unknownVariation<0x%08x>", variation); + } + } + + private static String toNumberVariationString(final int variation) { + switch (variation) { + case InputType.TYPE_NUMBER_VARIATION_NORMAL: + return "TYPE_NUMBER_VARIATION_NORMAL"; + case InputType.TYPE_NUMBER_VARIATION_PASSWORD: + return "TYPE_NUMBER_VARIATION_PASSWORD"; + default: + return String.format("unknownVariation<0x%08x>", variation); + } + } + + private static String toDatetimeVariationString(final int variation) { + switch (variation) { + case InputType.TYPE_DATETIME_VARIATION_NORMAL: + return "TYPE_DATETIME_VARIATION_NORMAL"; + case InputType.TYPE_DATETIME_VARIATION_DATE: + return "TYPE_DATETIME_VARIATION_DATE"; + case InputType.TYPE_DATETIME_VARIATION_TIME: + return "TYPE_DATETIME_VARIATION_TIME"; + default: + return String.format("unknownVariation<0x%08x>", variation); + } + } + + private static String toFlagsString(final int flags) { + final ArrayList<String> flagsArray = new ArrayList<>(); + if (0 != (flags & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS)) + flagsArray.add("TYPE_TEXT_FLAG_NO_SUGGESTIONS"); + if (0 != (flags & InputType.TYPE_TEXT_FLAG_MULTI_LINE)) + flagsArray.add("TYPE_TEXT_FLAG_MULTI_LINE"); + if (0 != (flags & InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE)) + flagsArray.add("TYPE_TEXT_FLAG_IME_MULTI_LINE"); + if (0 != (flags & InputType.TYPE_TEXT_FLAG_CAP_WORDS)) + flagsArray.add("TYPE_TEXT_FLAG_CAP_WORDS"); + if (0 != (flags & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)) + flagsArray.add("TYPE_TEXT_FLAG_CAP_SENTENCES"); + if (0 != (flags & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS)) + flagsArray.add("TYPE_TEXT_FLAG_CAP_CHARACTERS"); + if (0 != (flags & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT)) + flagsArray.add("TYPE_TEXT_FLAG_AUTO_CORRECT"); + if (0 != (flags & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE)) + flagsArray.add("TYPE_TEXT_FLAG_AUTO_COMPLETE"); + return flagsArray.isEmpty() ? "" : Arrays.toString(flagsArray.toArray()); } // Pretty print @Override public String toString() { - return "\n mInputTypeNoAutoCorrect = " + mInputTypeNoAutoCorrect - + "\n mIsSettingsSuggestionStripOn = " + mIsSettingsSuggestionStripOn - + "\n mApplicationSpecifiedCompletionOn = " + mApplicationSpecifiedCompletionOn; + return String.format( + "%s: inputType=0x%08x%s%s%s%s%s targetApp=%s\n", getClass().getSimpleName(), + mInputType, + (mInputTypeNoAutoCorrect ? " noAutoCorrect" : ""), + (mIsPasswordField ? " password" : ""), + (mShouldShowSuggestions ? " shouldShowSuggestions" : ""), + (mApplicationSpecifiedCompletionOn ? " appSpecified" : ""), + (mShouldInsertSpacesAutomatically ? " insertSpaces" : ""), + mTargetApplicationPackageName); } - public static boolean inPrivateImeOptions(String packageName, String key, - EditorInfo editorInfo) { + public static boolean inPrivateImeOptions(final String packageName, final String key, + final EditorInfo editorInfo) { if (editorInfo == null) return false; - final String findingKey = (packageName != null) ? packageName + "." + key - : key; + final String findingKey = (packageName != null) ? packageName + "." + key : key; return StringUtils.containsInCommaSplittableText(findingKey, editorInfo.privateImeOptions); } } diff --git a/java/src/com/android/inputmethod/latin/InputPointers.java b/java/src/com/android/inputmethod/latin/InputPointers.java index 2e638aaf3..47bc6b078 100644 --- a/java/src/com/android/inputmethod/latin/InputPointers.java +++ b/java/src/com/android/inputmethod/latin/InputPointers.java @@ -16,14 +16,17 @@ package com.android.inputmethod.latin; +import android.util.Log; +import android.util.SparseIntArray; + import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.utils.ResizableIntArray; -import android.util.Log; - // TODO: This class is not thread-safe. public final class InputPointers { private static final String TAG = InputPointers.class.getSimpleName(); + private static final boolean DEBUG_TIME = false; + private final int mDefaultCapacity; private final ResizableIntArray mXCoordinates; private final ResizableIntArray mYCoordinates; @@ -38,11 +41,29 @@ public final class InputPointers { mTimes = new ResizableIntArray(defaultCapacity); } - public void addPointer(int index, int x, int y, int pointerId, int time) { - mXCoordinates.add(index, x); - mYCoordinates.add(index, y); - mPointerIds.add(index, pointerId); - mTimes.add(index, time); + private void fillWithLastTimeUntil(final int index) { + final int fromIndex = mTimes.getLength(); + // Fill the gap with the latest time. + // See {@link #getTime(int)} and {@link #isValidTimeStamps()}. + if (fromIndex <= 0) { + return; + } + final int fillLength = index - fromIndex + 1; + if (fillLength <= 0) { + return; + } + final int lastTime = mTimes.get(fromIndex - 1); + mTimes.fill(lastTime, fromIndex, fillLength); + } + + public void addPointerAt(int index, int x, int y, int pointerId, int time) { + mXCoordinates.addAt(index, x); + mYCoordinates.addAt(index, y); + mPointerIds.addAt(index, pointerId); + if (LatinImeLogger.sDBG || DEBUG_TIME) { + fillWithLastTimeUntil(index); + } + mTimes.addAt(index, time); } @UsedForTesting @@ -68,23 +89,6 @@ public final class InputPointers { } /** - * Append the pointers in the specified {@link InputPointers} to the end of this. - * @param src the source {@link InputPointers} to read the data from. - * @param startPos the starting index of the pointers in {@code src}. - * @param length the number of pointers to be appended. - */ - @UsedForTesting - void append(InputPointers src, int startPos, int length) { - if (length == 0) { - return; - } - mXCoordinates.append(src.mXCoordinates, startPos, length); - mYCoordinates.append(src.mYCoordinates, startPos, length); - mPointerIds.append(src.mPointerIds, startPos, length); - mTimes.append(src.mTimes, startPos, length); - } - - /** * Append the times, x-coordinates and y-coordinates in the specified {@link ResizableIntArray} * to the end of this. * @param pointerId the pointer id of the source. @@ -141,7 +145,7 @@ public final class InputPointers { } public int[] getTimes() { - if (LatinImeLogger.sDBG) { + if (LatinImeLogger.sDBG || DEBUG_TIME) { if (!isValidTimeStamps()) { throw new RuntimeException("Time stamps are invalid."); } @@ -157,14 +161,21 @@ public final class InputPointers { private boolean isValidTimeStamps() { final int[] times = mTimes.getPrimitiveArray(); - for (int i = 1; i < getPointerSize(); ++i) { - if (times[i] < times[i - 1]) { + final int[] pointerIds = mPointerIds.getPrimitiveArray(); + final SparseIntArray lastTimeOfPointers = new SparseIntArray(); + final int size = getPointerSize(); + for (int i = 0; i < size; ++i) { + final int pointerId = pointerIds[i]; + final int time = times[i]; + final int lastTime = lastTimeOfPointers.get(pointerId, time); + if (time < lastTime) { // dump - for (int j = 0; j < times.length; ++j) { + for (int j = 0; j < size; ++j) { Log.d(TAG, "--- (" + j + ") " + times[j]); } return false; } + lastTimeOfPointers.put(pointerId, time); } return true; } diff --git a/java/src/com/android/inputmethod/latin/InputView.java b/java/src/com/android/inputmethod/latin/InputView.java index 81ccf83d8..e9e12f09f 100644 --- a/java/src/com/android/inputmethod/latin/InputView.java +++ b/java/src/com/android/inputmethod/latin/InputView.java @@ -23,87 +23,227 @@ import android.view.MotionEvent; import android.view.View; import android.widget.LinearLayout; -public final class InputView extends LinearLayout { - private View mSuggestionStripView; - private View mKeyboardView; - private int mKeyboardTopPadding; +import com.android.inputmethod.accessibility.AccessibilityUtils; +import com.android.inputmethod.keyboard.MainKeyboardView; +import com.android.inputmethod.latin.suggestions.MoreSuggestionsView; +import com.android.inputmethod.latin.suggestions.SuggestionStripView; - private boolean mIsForwardingEvent; +public final class InputView extends LinearLayout { private final Rect mInputViewRect = new Rect(); - private final Rect mEventForwardingRect = new Rect(); - private final Rect mEventReceivingRect = new Rect(); + private MainKeyboardView mMainKeyboardView; + private KeyboardTopPaddingForwarder mKeyboardTopPaddingForwarder; + private MoreSuggestionsViewCanceler mMoreSuggestionsViewCanceler; + private MotionEventForwarder<?, ?> mActiveForwarder; public InputView(final Context context, final AttributeSet attrs) { super(context, attrs, 0); } - public void setKeyboardGeometry(final int keyboardTopPadding) { - mKeyboardTopPadding = keyboardTopPadding; - } - @Override protected void onFinishInflate() { - mSuggestionStripView = findViewById(R.id.suggestion_strip_view); - mKeyboardView = findViewById(R.id.keyboard_view); + final SuggestionStripView suggestionStripView = + (SuggestionStripView)findViewById(R.id.suggestion_strip_view); + mMainKeyboardView = (MainKeyboardView)findViewById(R.id.keyboard_view); + mKeyboardTopPaddingForwarder = new KeyboardTopPaddingForwarder( + mMainKeyboardView, suggestionStripView); + mMoreSuggestionsViewCanceler = new MoreSuggestionsViewCanceler( + mMainKeyboardView, suggestionStripView); + } + + public void setKeyboardTopPadding(final int keyboardTopPadding) { + mKeyboardTopPaddingForwarder.setKeyboardTopPadding(keyboardTopPadding); } @Override - public boolean dispatchTouchEvent(final MotionEvent me) { - if (mSuggestionStripView.getVisibility() != VISIBLE - || mKeyboardView.getVisibility() != VISIBLE) { - return super.dispatchTouchEvent(me); + protected boolean dispatchHoverEvent(final MotionEvent event) { + if (AccessibilityUtils.getInstance().isTouchExplorationEnabled() + && mMainKeyboardView.isShowingMoreKeysPanel()) { + // With accessibility mode on, discard hover events while a more keys keyboard is shown. + // The {@link MoreKeysKeyboard} receives hover events directly from the platform. + return true; } + return super.dispatchHoverEvent(event); + } + + @Override + public boolean onInterceptTouchEvent(final MotionEvent me) { + final Rect rect = mInputViewRect; + getGlobalVisibleRect(rect); + final int index = me.getActionIndex(); + final int x = (int)me.getX(index) + rect.left; + final int y = (int)me.getY(index) + rect.top; // The touch events that hit the top padding of keyboard should be forwarded to // {@link SuggestionStripView}. + if (mKeyboardTopPaddingForwarder.onInterceptTouchEvent(x, y, me)) { + mActiveForwarder = mKeyboardTopPaddingForwarder; + return true; + } + + // To cancel {@link MoreSuggestionsView}, we should intercept a touch event to + // {@link MainKeyboardView} and dismiss the {@link MoreSuggestionsView}. + if (mMoreSuggestionsViewCanceler.onInterceptTouchEvent(x, y, me)) { + mActiveForwarder = mMoreSuggestionsViewCanceler; + return true; + } + + mActiveForwarder = null; + return false; + } + + @Override + public boolean onTouchEvent(final MotionEvent me) { + if (mActiveForwarder == null) { + return super.onTouchEvent(me); + } + final Rect rect = mInputViewRect; - this.getGlobalVisibleRect(rect); - final int x = (int)me.getX() + rect.left; - final int y = (int)me.getY() + rect.top; + getGlobalVisibleRect(rect); + final int index = me.getActionIndex(); + final int x = (int)me.getX(index) + rect.left; + final int y = (int)me.getY(index) + rect.top; + return mActiveForwarder.onTouchEvent(x, y, me); + } + + /** + * This class forwards series of {@link MotionEvent}s from <code>SenderView</code> to + * <code>ReceiverView</code>. + * + * @param <SenderView> a {@link View} that may send a {@link MotionEvent} to <ReceiverView>. + * @param <ReceiverView> a {@link View} that receives forwarded {@link MotionEvent} from + * <SenderView>. + */ + private static abstract class + MotionEventForwarder<SenderView extends View, ReceiverView extends View> { + protected final SenderView mSenderView; + protected final ReceiverView mReceiverView; - final Rect forwardingRect = mEventForwardingRect; - mKeyboardView.getGlobalVisibleRect(forwardingRect); - if (!mIsForwardingEvent && !forwardingRect.contains(x, y)) { - return super.dispatchTouchEvent(me); + protected final Rect mEventSendingRect = new Rect(); + protected final Rect mEventReceivingRect = new Rect(); + + public MotionEventForwarder(final SenderView senderView, final ReceiverView receiverView) { + mSenderView = senderView; + mReceiverView = receiverView; } - final int forwardingLimitY = forwardingRect.top + mKeyboardTopPadding; - boolean sendToTarget = false; + // Return true if a touch event of global coordinate x, y needs to be forwarded. + protected abstract boolean needsToForward(final int x, final int y); + + // Translate global x-coordinate to <code>ReceiverView</code> local coordinate. + protected int translateX(final int x) { + return x - mEventReceivingRect.left; + } - switch (me.getAction()) { - case MotionEvent.ACTION_DOWN: - if (y < forwardingLimitY) { - // This down event and further move and up events should be forwarded to the target. - mIsForwardingEvent = true; - sendToTarget = true; + // Translate global y-coordinate to <code>ReceiverView</code> local coordinate. + protected int translateY(final int y) { + return y - mEventReceivingRect.top; + } + + // Callback when a {@link MotionEvent} is forwarded. + protected void onForwardingEvent(final MotionEvent me) {} + + // Returns true if a {@link MotionEvent} is needed to be forwarded to + // <code>ReceiverView</code>. Otherwise returns false. + public boolean onInterceptTouchEvent(final int x, final int y, final MotionEvent me) { + // Forwards a {link MotionEvent} only if both <code>SenderView</code> and + // <code>ReceiverView</code> are visible. + if (mSenderView.getVisibility() != View.VISIBLE || + mReceiverView.getVisibility() != View.VISIBLE) { + return false; + } + mSenderView.getGlobalVisibleRect(mEventSendingRect); + if (!mEventSendingRect.contains(x, y)) { + return false; } - break; - case MotionEvent.ACTION_MOVE: - sendToTarget = mIsForwardingEvent; - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - sendToTarget = mIsForwardingEvent; - mIsForwardingEvent = false; - break; - } - - if (!sendToTarget) { - return super.dispatchTouchEvent(me); - } - - final Rect receivingRect = mEventReceivingRect; - mSuggestionStripView.getGlobalVisibleRect(receivingRect); - final int translatedX = x - receivingRect.left; - final int translatedY; - if (y < forwardingLimitY) { - // The forwarded event should have coordinates that are inside of the target. - translatedY = Math.min(y - receivingRect.top, receivingRect.height() - 1); - } else { - translatedY = y - receivingRect.top; - } - me.setLocation(translatedX, translatedY); - mSuggestionStripView.dispatchTouchEvent(me); - return true; + + if (me.getActionMasked() == MotionEvent.ACTION_DOWN) { + // If the down event happens in the forwarding area, successive + // {@link MotionEvent}s should be forwarded to <code>ReceiverView</code>. + if (needsToForward(x, y)) { + return true; + } + } + + return false; + } + + // Returns true if a {@link MotionEvent} is forwarded to <code>ReceiverView</code>. + // Otherwise returns false. + public boolean onTouchEvent(final int x, final int y, final MotionEvent me) { + mReceiverView.getGlobalVisibleRect(mEventReceivingRect); + // Translate global coordinates to <code>ReceiverView</code> local coordinates. + me.setLocation(translateX(x), translateY(y)); + mReceiverView.dispatchTouchEvent(me); + onForwardingEvent(me); + return true; + } + } + + /** + * This class forwards {@link MotionEvent}s happened in the top padding of + * {@link MainKeyboardView} to {@link SuggestionStripView}. + */ + private static class KeyboardTopPaddingForwarder + extends MotionEventForwarder<MainKeyboardView, SuggestionStripView> { + private int mKeyboardTopPadding; + + public KeyboardTopPaddingForwarder(final MainKeyboardView mainKeyboardView, + final SuggestionStripView suggestionStripView) { + super(mainKeyboardView, suggestionStripView); + } + + public void setKeyboardTopPadding(final int keyboardTopPadding) { + mKeyboardTopPadding = keyboardTopPadding; + } + + private boolean isInKeyboardTopPadding(final int y) { + return y < mEventSendingRect.top + mKeyboardTopPadding; + } + + @Override + protected boolean needsToForward(final int x, final int y) { + // Forwarding an event only when {@link MainKeyboardView} is visible. + // Because the visibility of {@link MainKeyboardView} is controlled by its parent + // view in {@link KeyboardSwitcher#setMainKeyboardFrame()}, we should check the + // visibility of the parent view. + final View mainKeyboardFrame = (View)mSenderView.getParent(); + return mainKeyboardFrame.getVisibility() == View.VISIBLE && isInKeyboardTopPadding(y); + } + + @Override + protected int translateY(final int y) { + final int translatedY = super.translateY(y); + if (isInKeyboardTopPadding(y)) { + // The forwarded event should have coordinates that are inside of the target. + return Math.min(translatedY, mEventReceivingRect.height() - 1); + } + return translatedY; + } + } + + /** + * This class forwards {@link MotionEvent}s happened in the {@link MainKeyboardView} to + * {@link SuggestionStripView} when the {@link MoreSuggestionsView} is showing. + * {@link SuggestionStripView} dismisses {@link MoreSuggestionsView} when it receives any event + * outside of it. + */ + private static class MoreSuggestionsViewCanceler + extends MotionEventForwarder<MainKeyboardView, SuggestionStripView> { + public MoreSuggestionsViewCanceler(final MainKeyboardView mainKeyboardView, + final SuggestionStripView suggestionStripView) { + super(mainKeyboardView, suggestionStripView); + } + + @Override + protected boolean needsToForward(final int x, final int y) { + return mReceiverView.isShowingMoreSuggestionPanel() && mEventSendingRect.contains(x, y); + } + + @Override + protected void onForwardingEvent(final MotionEvent me) { + if (me.getActionMasked() == MotionEvent.ACTION_DOWN) { + mReceiverView.dismissMoreSuggestionsPanel(); + } + } } } diff --git a/java/src/com/android/inputmethod/latin/LastComposedWord.java b/java/src/com/android/inputmethod/latin/LastComposedWord.java index 2e9280c77..8cbf8379b 100644 --- a/java/src/com/android/inputmethod/latin/LastComposedWord.java +++ b/java/src/com/android/inputmethod/latin/LastComposedWord.java @@ -18,6 +18,10 @@ package com.android.inputmethod.latin; import android.text.TextUtils; +import com.android.inputmethod.event.Event; + +import java.util.ArrayList; + /** * This class encapsulates data about a word previously composed, but that has been * committed already. This is used for resuming suggestion, and cancel auto-correction. @@ -40,11 +44,11 @@ public final class LastComposedWord { public static final String NOT_A_SEPARATOR = ""; - public final int[] mPrimaryKeyCodes; + public final ArrayList<Event> mEvents; public final String mTypedWord; - public final String mCommittedWord; + public final CharSequence mCommittedWord; public final String mSeparatorString; - public final String mPrevWord; + public final PrevWordsInfo mPrevWordsInfo; public final int mCapitalizedMode; public final InputPointers mInputPointers = new InputPointers(Constants.DICTIONARY_MAX_WORD_LENGTH); @@ -52,23 +56,24 @@ public final class LastComposedWord { private boolean mActive; public static final LastComposedWord NOT_A_COMPOSED_WORD = - new LastComposedWord(null, null, "", "", NOT_A_SEPARATOR, null, - WordComposer.CAPS_MODE_OFF); + new LastComposedWord(new ArrayList<Event>(), null, "", "", + NOT_A_SEPARATOR, null, WordComposer.CAPS_MODE_OFF); // Warning: this is using the passed objects as is and fully expects them to be // immutable. Do not fiddle with their contents after you passed them to this constructor. - public LastComposedWord(final int[] primaryKeyCodes, final InputPointers inputPointers, - final String typedWord, final String committedWord, final String separatorString, - final String prevWord, final int capitalizedMode) { - mPrimaryKeyCodes = primaryKeyCodes; + public LastComposedWord(final ArrayList<Event> events, + final InputPointers inputPointers, final String typedWord, + final CharSequence committedWord, final String separatorString, + final PrevWordsInfo prevWordsInfo, final int capitalizedMode) { if (inputPointers != null) { mInputPointers.copy(inputPointers); } mTypedWord = typedWord; + mEvents = new ArrayList<>(events); mCommittedWord = committedWord; mSeparatorString = separatorString; mActive = true; - mPrevWord = prevWord; + mPrevWordsInfo = prevWordsInfo; mCapitalizedMode = capitalizedMode; } diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 77d07019f..d2c4ca712 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -25,10 +25,9 @@ import android.app.AlertDialog; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.content.pm.PackageInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; @@ -36,39 +35,31 @@ import android.inputmethodservice.InputMethodService; import android.media.AudioManager; import android.net.ConnectivityManager; import android.os.Debug; -import android.os.Handler; -import android.os.HandlerThread; import android.os.IBinder; import android.os.Message; -import android.os.SystemClock; -import android.preference.PreferenceManager; import android.text.InputType; import android.text.TextUtils; -import android.text.style.SuggestionSpan; import android.util.Log; -import android.util.Pair; import android.util.PrintWriterPrinter; import android.util.Printer; -import android.view.KeyCharacterMap; +import android.util.SparseArray; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup.LayoutParams; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.CompletionInfo; -import android.view.inputmethod.CorrectionInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.accessibility.AccessibilityUtils; -import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy; import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.compat.AppWorkaroundsUtils; import com.android.inputmethod.compat.InputMethodServiceCompatUtils; -import com.android.inputmethod.compat.SuggestionSpanUtils; import com.android.inputmethod.dictionarypack.DictionaryPackConstants; -import com.android.inputmethod.event.EventInterpreter; -import com.android.inputmethod.keyboard.KeyDetector; +import com.android.inputmethod.event.Event; +import com.android.inputmethod.event.HardwareEventDecoder; +import com.android.inputmethod.event.HardwareKeyboardEventDecoder; +import com.android.inputmethod.event.InputTransaction; import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardActionListener; import com.android.inputmethod.keyboard.KeyboardId; @@ -77,63 +68,53 @@ import com.android.inputmethod.keyboard.MainKeyboardView; import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.define.ProductionFlag; +import com.android.inputmethod.latin.inputlogic.InputLogic; +import com.android.inputmethod.latin.personalization.ContextualDictionaryUpdater; import com.android.inputmethod.latin.personalization.DictionaryDecayBroadcastReciever; -import com.android.inputmethod.latin.personalization.PersonalizationDictionary; -import com.android.inputmethod.latin.personalization.PersonalizationDictionarySessionRegister; +import com.android.inputmethod.latin.personalization.PersonalizationDictionaryUpdater; import com.android.inputmethod.latin.personalization.PersonalizationHelper; -import com.android.inputmethod.latin.personalization.PersonalizationPredictionDictionary; -import com.android.inputmethod.latin.personalization.UserHistoryDictionary; import com.android.inputmethod.latin.settings.Settings; import com.android.inputmethod.latin.settings.SettingsActivity; import com.android.inputmethod.latin.settings.SettingsValues; import com.android.inputmethod.latin.suggestions.SuggestionStripView; +import com.android.inputmethod.latin.suggestions.SuggestionStripViewAccessor; import com.android.inputmethod.latin.utils.ApplicationUtils; -import com.android.inputmethod.latin.utils.AsyncResultHolder; -import com.android.inputmethod.latin.utils.AutoCorrectionUtils; import com.android.inputmethod.latin.utils.CapsModeUtils; -import com.android.inputmethod.latin.utils.CollectionUtils; -import com.android.inputmethod.latin.utils.CompletionInfoUtils; -import com.android.inputmethod.latin.utils.InputTypeUtils; +import com.android.inputmethod.latin.utils.CoordinateUtils; +import com.android.inputmethod.latin.utils.DialogUtils; +import com.android.inputmethod.latin.utils.DistracterFilterCheckingExactMatches; +import com.android.inputmethod.latin.utils.ImportantNoticeUtils; import com.android.inputmethod.latin.utils.IntentUtils; import com.android.inputmethod.latin.utils.JniUtils; -import com.android.inputmethod.latin.utils.LatinImeLoggerUtils; -import com.android.inputmethod.latin.utils.RecapitalizeStatus; -import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper; -import com.android.inputmethod.latin.utils.StringUtils; -import com.android.inputmethod.latin.utils.TargetPackageInfoGetterTask; -import com.android.inputmethod.latin.utils.TextRange; -import com.android.inputmethod.latin.utils.UserHistoryForgettingCurveUtils; -import com.android.inputmethod.research.ResearchLogger; +import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper; +import com.android.inputmethod.latin.utils.StatsUtils; +import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.List; import java.util.Locale; -import java.util.TreeSet; +import java.util.concurrent.TimeUnit; /** * Input method implementation for Qwerty'ish keyboard. */ public class LatinIME extends InputMethodService implements KeyboardActionListener, - SuggestionStripView.Listener, TargetPackageInfoGetterTask.OnTargetPackageInfoKnownListener, - Suggest.SuggestInitializationListener { + SuggestionStripView.Listener, SuggestionStripViewAccessor, + DictionaryFacilitator.DictionaryInitializationListener, + ImportantNoticeDialog.ImportantNoticeDialogListener { private static final String TAG = LatinIME.class.getSimpleName(); private static final boolean TRACE = false; - private static boolean DEBUG; + private static boolean DEBUG = false; private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100; - // How many continuous deletes at which to start deleting at a higher speed. - private static final int DELETE_ACCELERATE_AT = 20; - // Key events coming any faster than this are long-presses. - private static final int QUICK_PRESS = 200; - private static final int PENDING_IMS_CALLBACK_DURATION = 800; - private static final int PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT = 2; + private static final int DELAY_WAIT_FOR_DICTIONARY_LOAD = 2000; // 2s - // TODO: Set this value appropriately. - private static final int GET_SUGGESTED_WORDS_TIMEOUT = 200; + private static final int PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT = 2; /** * The name of the scheme used by the Package Manager to warn of a new package installation, @@ -141,168 +122,142 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen */ private static final String SCHEME_PACKAGE = "package"; - private static final int SPACE_STATE_NONE = 0; - // Double space: the state where the user pressed space twice quickly, which LatinIME - // resolved as period-space. Undoing this converts the period to a space. - private static final int SPACE_STATE_DOUBLE = 1; - // Swap punctuation: the state where a weak space and a punctuation from the suggestion strip - // have just been swapped. Undoing this swaps them back; the space is still considered weak. - private static final int SPACE_STATE_SWAP_PUNCTUATION = 2; - // Weak space: a space that should be swapped only by suggestion strip punctuation. Weak - // spaces happen when the user presses space, accepting the current suggestion (whether - // it's an auto-correction or not). - private static final int SPACE_STATE_WEAK = 3; - // Phantom space: a not-yet-inserted space that should get inserted on the next input, - // character provided it's not a separator. If it's a separator, the phantom space is dropped. - // Phantom spaces happen when a user chooses a word from the suggestion strip. - private static final int SPACE_STATE_PHANTOM = 4; - - // Current space state of the input method. This can be any of the above constants. - private int mSpaceState; - private final Settings mSettings; + private final DictionaryFacilitator mDictionaryFacilitator = + new DictionaryFacilitator(new DistracterFilterCheckingExactMatches(this /* context */)); + // TODO: Move from LatinIME. + private final PersonalizationDictionaryUpdater mPersonalizationDictionaryUpdater = + new PersonalizationDictionaryUpdater(this /* context */, mDictionaryFacilitator); + private final ContextualDictionaryUpdater mContextualDictionaryUpdater = + new ContextualDictionaryUpdater(this /* context */, mDictionaryFacilitator, + new Runnable() { + @Override + public void run() { + mHandler.postUpdateSuggestionStrip(); + } + }); + private final InputLogic mInputLogic = new InputLogic(this /* LatinIME */, + this /* SuggestionStripViewAccessor */, mDictionaryFacilitator); + // We expect to have only one decoder in almost all cases, hence the default capacity of 1. + // If it turns out we need several, it will get grown seamlessly. + final SparseArray<HardwareEventDecoder> mHardwareEventDecoders = new SparseArray<>(1); private View mExtractArea; private View mKeyPreviewBackingView; private SuggestionStripView mSuggestionStripView; - // Never null - private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY; - private Suggest mSuggest; - private CompletionInfo[] mApplicationSpecifiedCompletions; - private AppWorkaroundsUtils mAppWorkAroundsUtils = new AppWorkaroundsUtils(); private RichInputMethodManager mRichImm; @UsedForTesting final KeyboardSwitcher mKeyboardSwitcher; private final SubtypeSwitcher mSubtypeSwitcher; private final SubtypeState mSubtypeState = new SubtypeState(); - // At start, create a default event interpreter that does nothing by passing it no decoder spec. - // The event interpreter should never be null. - private EventInterpreter mEventInterpreter = new EventInterpreter(this); - - private boolean mIsMainDictionaryAvailable; - private UserBinaryDictionary mUserDictionary; - private UserHistoryDictionary mUserHistoryDictionary; - private PersonalizationPredictionDictionary mPersonalizationPredictionDictionary; - private PersonalizationDictionary mPersonalizationDictionary; - private boolean mIsUserDictionaryAvailable; - - private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; - private final WordComposer mWordComposer = new WordComposer(); - private final RichInputConnection mConnection = new RichInputConnection(this); - private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus(); - - // Keep track of the last selection range to decide if we need to show word alternatives - private static final int NOT_A_CURSOR_POSITION = -1; - private int mLastSelectionStart = NOT_A_CURSOR_POSITION; - private int mLastSelectionEnd = NOT_A_CURSOR_POSITION; - - // Whether we are expecting an onUpdateSelection event to fire. If it does when we don't - // "expect" it, it means the user actually moved the cursor. - private boolean mExpectingUpdateSelection; - private int mDeleteCount; - private long mLastKeyTime; - private final TreeSet<Long> mCurrentlyPressedHardwareKeys = CollectionUtils.newTreeSet(); - // Personalization debugging params - private boolean mUseOnlyPersonalizationDictionaryForDebug = false; - private boolean mBoostPersonalizationDictionaryForDebug = false; - - // Member variables for remembering the current device orientation. - private int mDisplayOrientation; // Object for reacting to adding/removing a dictionary pack. - private BroadcastReceiver mDictionaryPackInstallReceiver = + private final BroadcastReceiver mDictionaryPackInstallReceiver = new DictionaryPackInstallBroadcastReceiver(this); - // Keeps track of most recently inserted text (multi-character key) for reverting - private String mEnteredText; - - // TODO: This boolean is persistent state and causes large side effects at unexpected times. - // Find a way to remove it for readability. - private boolean mIsAutoCorrectionIndicatorOn; + private final BroadcastReceiver mDictionaryDumpBroadcastReceiver = + new DictionaryDumpBroadcastReceiver(this); private AlertDialog mOptionsDialog; private final boolean mIsHardwareAcceleratedDrawingEnabled; public final UIHandler mHandler = new UIHandler(this); - private InputUpdater mInputUpdater; - public static final class UIHandler extends StaticInnerHandlerWrapper<LatinIME> { + public static final class UIHandler extends LeakGuardHandlerWrapper<LatinIME> { private static final int MSG_UPDATE_SHIFT_STATE = 0; private static final int MSG_PENDING_IMS_CALLBACK = 1; private static final int MSG_UPDATE_SUGGESTION_STRIP = 2; private static final int MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 3; private static final int MSG_RESUME_SUGGESTIONS = 4; private static final int MSG_REOPEN_DICTIONARIES = 5; - private static final int MSG_ON_END_BATCH_INPUT = 6; + private static final int MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED = 6; private static final int MSG_RESET_CACHES = 7; + private static final int MSG_WAIT_FOR_DICTIONARY_LOAD = 8; + // Update this when adding new messages + private static final int MSG_LAST = MSG_WAIT_FOR_DICTIONARY_LOAD; private static final int ARG1_NOT_GESTURE_INPUT = 0; private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1; private static final int ARG1_SHOW_GESTURE_FLOATING_PREVIEW_TEXT = 2; - private static final int ARG2_WITHOUT_TYPED_WORD = 0; - private static final int ARG2_WITH_TYPED_WORD = 1; + private static final int ARG2_UNUSED = 0; + private static final int ARG1_FALSE = 0; + private static final int ARG1_TRUE = 1; private int mDelayUpdateSuggestions; private int mDelayUpdateShiftState; - private long mDoubleSpacePeriodTimeout; - private long mDoubleSpacePeriodTimerStart; - public UIHandler(final LatinIME outerInstance) { - super(outerInstance); + public UIHandler(final LatinIME ownerInstance) { + super(ownerInstance); } public void onCreate() { - final Resources res = getOuterInstance().getResources(); - mDelayUpdateSuggestions = - res.getInteger(R.integer.config_delay_update_suggestions); - mDelayUpdateShiftState = - res.getInteger(R.integer.config_delay_update_shift_state); - mDoubleSpacePeriodTimeout = - res.getInteger(R.integer.config_double_space_period_timeout); + final LatinIME latinIme = getOwnerInstance(); + if (latinIme == null) { + return; + } + final Resources res = latinIme.getResources(); + mDelayUpdateSuggestions = res.getInteger(R.integer.config_delay_update_suggestions); + mDelayUpdateShiftState = res.getInteger(R.integer.config_delay_update_shift_state); } @Override public void handleMessage(final Message msg) { - final LatinIME latinIme = getOuterInstance(); + final LatinIME latinIme = getOwnerInstance(); + if (latinIme == null) { + return; + } final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher; switch (msg.what) { case MSG_UPDATE_SUGGESTION_STRIP: - latinIme.updateSuggestionStrip(); + cancelUpdateSuggestionStrip(); + latinIme.mInputLogic.performUpdateSuggestionStripSync( + latinIme.mSettings.getCurrent()); break; case MSG_UPDATE_SHIFT_STATE: - switcher.updateShiftState(); + switcher.requestUpdatingShiftState(latinIme.getCurrentAutoCapsState(), + latinIme.getCurrentRecapitalizeState()); break; case MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP: if (msg.arg1 == ARG1_NOT_GESTURE_INPUT) { - if (msg.arg2 == ARG2_WITH_TYPED_WORD) { - final Pair<SuggestedWords, String> p = - (Pair<SuggestedWords, String>) msg.obj; - latinIme.showSuggestionStripWithTypedWord(p.first, p.second); - } else { - latinIme.showSuggestionStrip((SuggestedWords) msg.obj); - } + final SuggestedWords suggestedWords = (SuggestedWords) msg.obj; + latinIme.showSuggestionStrip(suggestedWords); } else { latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords) msg.obj, msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT); } break; case MSG_RESUME_SUGGESTIONS: - latinIme.restartSuggestionsOnWordTouchedByCursor(); + latinIme.mInputLogic.restartSuggestionsOnWordTouchedByCursor( + latinIme.mSettings.getCurrent(), + msg.arg1 == ARG1_TRUE /* shouldIncludeResumedWordInSuggestions */, + latinIme.mKeyboardSwitcher.getCurrentKeyboardScriptId()); break; case MSG_REOPEN_DICTIONARIES: - latinIme.initSuggest(); - // In theory we could call latinIme.updateSuggestionStrip() right away, but - // in the practice, the dictionary is not finished opening yet so we wouldn't - // get any suggestions. Wait one frame. - postUpdateSuggestionStrip(); + latinIme.resetSuggest(); + // We need to re-evaluate the currently composing word in case the script has + // changed. + postWaitForDictionaryLoad(); break; - case MSG_ON_END_BATCH_INPUT: - latinIme.onEndBatchInputAsyncInternal((SuggestedWords) msg.obj); + case MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED: + latinIme.mInputLogic.onUpdateTailBatchInputCompleted( + latinIme.mSettings.getCurrent(), + (SuggestedWords) msg.obj, latinIme.mKeyboardSwitcher); break; case MSG_RESET_CACHES: - latinIme.retryResetCaches(msg.arg1 == 1 /* tryResumeSuggestions */, - msg.arg2 /* remainingTries */); + final SettingsValues settingsValues = latinIme.mSettings.getCurrent(); + if (latinIme.mInputLogic.retryResetCachesAndReturnSuccess(settingsValues, + msg.arg1 == 1 /* tryResumeSuggestions */, + msg.arg2 /* remainingTries */, this /* handler */)) { + // If we were able to reset the caches, then we can reload the keyboard. + // Otherwise, we'll do it when we can. + latinIme.mKeyboardSwitcher.loadKeyboard(latinIme.getCurrentInputEditorInfo(), + settingsValues, latinIme.getCurrentAutoCapsState(), + latinIme.getCurrentRecapitalizeState()); + } + break; + case MSG_WAIT_FOR_DICTIONARY_LOAD: + Log.i(TAG, "Timeout waiting for dictionary load"); break; } } @@ -315,9 +270,27 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen sendMessage(obtainMessage(MSG_REOPEN_DICTIONARIES)); } - public void postResumeSuggestions() { + public void postResumeSuggestions(final boolean shouldIncludeResumedWordInSuggestions, + final boolean shouldDelay) { + final LatinIME latinIme = getOwnerInstance(); + if (latinIme == null) { + return; + } + if (!latinIme.mSettings.getCurrent() + .isCurrentOrientationAllowingSuggestionsPerUserSettings()) { + return; + } removeMessages(MSG_RESUME_SUGGESTIONS); - sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS), mDelayUpdateSuggestions); + if (shouldDelay) { + sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS, + shouldIncludeResumedWordInSuggestions ? ARG1_TRUE : ARG1_FALSE, + 0 /* ignored */), + mDelayUpdateSuggestions); + } else { + sendMessage(obtainMessage(MSG_RESUME_SUGGESTIONS, + shouldIncludeResumedWordInSuggestions ? ARG1_TRUE : ARG1_FALSE, + 0 /* ignored */)); + } } public void postResetCaches(final boolean tryResumeSuggestions, final int remainingTries) { @@ -326,6 +299,19 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen remainingTries, null)); } + public void postWaitForDictionaryLoad() { + sendMessageDelayed(obtainMessage(MSG_WAIT_FOR_DICTIONARY_LOAD), + DELAY_WAIT_FOR_DICTIONARY_LOAD); + } + + public void cancelWaitForDictionaryLoad() { + removeMessages(MSG_WAIT_FOR_DICTIONARY_LOAD); + } + + public boolean hasPendingWaitForDictionaryLoad() { + return hasMessages(MSG_WAIT_FOR_DICTIONARY_LOAD); + } + public void cancelUpdateSuggestionStrip() { removeMessages(MSG_UPDATE_SUGGESTION_STRIP); } @@ -343,8 +329,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), mDelayUpdateShiftState); } - public void cancelUpdateShiftState() { - removeMessages(MSG_UPDATE_SHIFT_STATE); + @UsedForTesting + public void removeAllMessages() { + for (int i = 0; i <= MSG_LAST; ++i) { + removeMessages(i); + } } public void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords, @@ -354,39 +343,17 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen ? ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT : ARG1_SHOW_GESTURE_FLOATING_PREVIEW_TEXT; obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, arg1, - ARG2_WITHOUT_TYPED_WORD, suggestedWords).sendToTarget(); + ARG2_UNUSED, suggestedWords).sendToTarget(); } public void showSuggestionStrip(final SuggestedWords suggestedWords) { removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, - ARG1_NOT_GESTURE_INPUT, ARG2_WITHOUT_TYPED_WORD, suggestedWords).sendToTarget(); - } - - // TODO: Remove this method. - public void showSuggestionStripWithTypedWord(final SuggestedWords suggestedWords, - final String typedWord) { - removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); - obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, ARG1_NOT_GESTURE_INPUT, - ARG2_WITH_TYPED_WORD, - new Pair<SuggestedWords, String>(suggestedWords, typedWord)).sendToTarget(); - } - - public void onEndBatchInput(final SuggestedWords suggestedWords) { - obtainMessage(MSG_ON_END_BATCH_INPUT, suggestedWords).sendToTarget(); - } - - public void startDoubleSpacePeriodTimer() { - mDoubleSpacePeriodTimerStart = SystemClock.uptimeMillis(); + ARG1_NOT_GESTURE_INPUT, ARG2_UNUSED, suggestedWords).sendToTarget(); } - public void cancelDoubleSpacePeriodTimer() { - mDoubleSpacePeriodTimerStart = 0; - } - - public boolean isAcceptingDoubleSpacePeriod() { - return SystemClock.uptimeMillis() - mDoubleSpacePeriodTimerStart - < mDoubleSpacePeriodTimeout; + public void showTailBatchInputResult(final SuggestedWords suggestedWords) { + obtainMessage(MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED, suggestedWords).sendToTarget(); } // Working variables for the following methods. @@ -401,7 +368,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen removeMessages(MSG_PENDING_IMS_CALLBACK); resetPendingImsCallback(); mIsOrientationChanging = true; - final LatinIME latinIme = getOuterInstance(); + final LatinIME latinIme = getOwnerInstance(); + if (latinIme == null) { + return; + } if (latinIme.isInputViewShown()) { latinIme.mKeyboardSwitcher.saveKeyboardState(); } @@ -415,12 +385,15 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private void executePendingImsCallback(final LatinIME latinIme, final EditorInfo editorInfo, boolean restarting) { - if (mHasPendingFinishInputView) + if (mHasPendingFinishInputView) { latinIme.onFinishInputViewInternal(mHasPendingFinishInput); - if (mHasPendingFinishInput) + } + if (mHasPendingFinishInput) { latinIme.onFinishInputInternal(); - if (mHasPendingStartInput) + } + if (mHasPendingStartInput) { latinIme.onStartInputInternal(editorInfo, restarting); + } resetPendingImsCallback(); } @@ -434,9 +407,17 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mIsOrientationChanging = false; mPendingSuccessiveImsCallback = true; } - final LatinIME latinIme = getOuterInstance(); - executePendingImsCallback(latinIme, editorInfo, restarting); - latinIme.onStartInputInternal(editorInfo, restarting); + final LatinIME latinIme = getOwnerInstance(); + if (latinIme != null) { + executePendingImsCallback(latinIme, editorInfo, restarting); + latinIme.onStartInputInternal(editorInfo, restarting); + if (ProductionFlag.USES_CURSOR_ANCHOR_MONITOR) { + // Currently we need to call this every time when the IME is attached to + // new application. + // TODO: Consider if we can do this automatically in the framework. + InputMethodServiceCompatUtils.setCursorAnchorMonitorMode(latinIme, 1); + } + } } } @@ -453,10 +434,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK), PENDING_IMS_CALLBACK_DURATION); } - final LatinIME latinIme = getOuterInstance(); - executePendingImsCallback(latinIme, editorInfo, restarting); - latinIme.onStartInputViewInternal(editorInfo, restarting); - mAppliedEditorInfo = editorInfo; + final LatinIME latinIme = getOwnerInstance(); + if (latinIme != null) { + executePendingImsCallback(latinIme, editorInfo, restarting); + latinIme.onStartInputViewInternal(editorInfo, restarting); + mAppliedEditorInfo = editorInfo; + } } } @@ -465,9 +448,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Typically this is the first onFinishInputView after orientation changed. mHasPendingFinishInputView = true; } else { - final LatinIME latinIme = getOuterInstance(); - latinIme.onFinishInputViewInternal(finishingInput); - mAppliedEditorInfo = null; + final LatinIME latinIme = getOwnerInstance(); + if (latinIme != null) { + latinIme.onFinishInputViewInternal(finishingInput); + mAppliedEditorInfo = null; + } } } @@ -476,31 +461,33 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Typically this is the first onFinishInput after orientation changed. mHasPendingFinishInput = true; } else { - final LatinIME latinIme = getOuterInstance(); - executePendingImsCallback(latinIme, null, false); - latinIme.onFinishInputInternal(); + final LatinIME latinIme = getOwnerInstance(); + if (latinIme != null) { + executePendingImsCallback(latinIme, null, false); + latinIme.onFinishInputInternal(); + } } } } static final class SubtypeState { private InputMethodSubtype mLastActiveSubtype; - private boolean mCurrentSubtypeUsed; + private boolean mCurrentSubtypeHasBeenUsed; - public void currentSubtypeUsed() { - mCurrentSubtypeUsed = true; + public void setCurrentSubtypeHasBeenUsed() { + mCurrentSubtypeHasBeenUsed = true; } public void switchSubtype(final IBinder token, final RichInputMethodManager richImm) { final InputMethodSubtype currentSubtype = richImm.getInputMethodManager() .getCurrentInputMethodSubtype(); final InputMethodSubtype lastActiveSubtype = mLastActiveSubtype; - final boolean currentSubtypeUsed = mCurrentSubtypeUsed; - if (currentSubtypeUsed) { + final boolean currentSubtypeHasBeenUsed = mCurrentSubtypeHasBeenUsed; + if (currentSubtypeHasBeenUsed) { mLastActiveSubtype = currentSubtype; - mCurrentSubtypeUsed = false; + mCurrentSubtypeHasBeenUsed = false; } - if (currentSubtypeUsed + if (currentSubtypeHasBeenUsed && richImm.checkIfSubtypeBelongsToThisImeAndEnabled(lastActiveSubtype) && !currentSubtype.equals(lastActiveSubtype)) { richImm.setInputMethodAndSubtype(token, lastActiveSubtype); @@ -536,7 +523,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen KeyboardSwitcher.init(this); AudioAndHapticFeedbackManager.init(this); AccessibilityUtils.init(this); - PersonalizationDictionarySessionRegister.init(this); + StatsUtils.init(this); super.onCreate(); @@ -545,19 +532,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // TODO: Resolve mutual dependencies of {@link #loadSettings()} and {@link #initSuggest()}. loadSettings(); - initSuggest(); - - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.getInstance().init(this, mKeyboardSwitcher, mSuggest); - } - mDisplayOrientation = getResources().getConfiguration().orientation; + resetSuggest(); // Register to receive ringer mode change and network state change. // Also receive installation and removal of a dictionary pack. final IntentFilter filter = new IntentFilter(); filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); - registerReceiver(mReceiver, filter); + registerReceiver(mConnectivityAndRingerModeChangeReceiver, filter); final IntentFilter packageFilter = new IntentFilter(); packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); @@ -569,47 +551,74 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen newDictFilter.addAction(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION); registerReceiver(mDictionaryPackInstallReceiver, newDictFilter); + final IntentFilter dictDumpFilter = new IntentFilter(); + dictDumpFilter.addAction(DictionaryDumpBroadcastReceiver.DICTIONARY_DUMP_INTENT_ACTION); + registerReceiver(mDictionaryDumpBroadcastReceiver, dictDumpFilter); + DictionaryDecayBroadcastReciever.setUpIntervalAlarmForDictionaryDecaying(this); - mInputUpdater = new InputUpdater(this); + StatsUtils.onCreate(mSettings.getCurrent()); } // Has to be package-visible for unit tests @UsedForTesting void loadSettings() { final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale(); - final InputAttributes inputAttributes = - new InputAttributes(getCurrentInputEditorInfo(), isFullscreenMode()); - mSettings.loadSettings(locale, inputAttributes); - AudioAndHapticFeedbackManager.getInstance().onSettingsChanged(mSettings.getCurrent()); - // To load the keyboard we need to load all the settings once, but resetting the - // contacts dictionary should be deferred until after the new layout has been displayed - // to improve responsivity. In the language switching process, we post a reopenDictionaries - // message, then come here to read the settings for the new language before we change - // the layout; at this time, we need to skip resetting the contacts dictionary. It will - // be done later inside {@see #initSuggest()} when the reopenDictionaries message is - // processed. + final EditorInfo editorInfo = getCurrentInputEditorInfo(); + final InputAttributes inputAttributes = new InputAttributes( + editorInfo, isFullscreenMode(), getPackageName()); + mSettings.loadSettings(this, locale, inputAttributes); + final SettingsValues currentSettingsValues = mSettings.getCurrent(); + AudioAndHapticFeedbackManager.getInstance().onSettingsChanged(currentSettingsValues); + // This method is called on startup and language switch, before the new layout has + // been displayed. Opening dictionaries never affects responsivity as dictionaries are + // asynchronously loaded. if (!mHandler.hasPendingReopenDictionaries()) { - // May need to reset the contacts dictionary depending on the user settings. - resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary()); + resetSuggestForLocale(locale); + } + mDictionaryFacilitator.updateEnabledSubtypes(mRichImm.getMyEnabledInputMethodSubtypeList( + true /* allowsImplicitlySelectedSubtypes */)); + refreshPersonalizationDictionarySession(currentSettingsValues); + StatsUtils.onLoadSettings(currentSettingsValues); + } + + private void refreshPersonalizationDictionarySession( + final SettingsValues currentSettingsValues) { + mPersonalizationDictionaryUpdater.onLoadSettings( + currentSettingsValues.mUsePersonalizedDicts, + mSubtypeSwitcher.isSystemLocaleSameAsLocaleOfAllEnabledSubtypesOfEnabledImes()); + mContextualDictionaryUpdater.onLoadSettings(currentSettingsValues.mUsePersonalizedDicts); + final boolean shouldKeepUserHistoryDictionaries; + if (currentSettingsValues.mUsePersonalizedDicts) { + shouldKeepUserHistoryDictionaries = true; + } else { + shouldKeepUserHistoryDictionaries = false; + } + if (!shouldKeepUserHistoryDictionaries) { + // Remove user history dictionaries. + PersonalizationHelper.removeAllUserHistoryDictionaries(this); + mDictionaryFacilitator.clearUserHistoryDictionary(); } } // 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); } + if (mHandler.hasPendingWaitForDictionaryLoad()) { + mHandler.cancelWaitForDictionaryLoad(); + mHandler.postResumeSuggestions(true /* shouldIncludeResumedWordInSuggestions */, + false /* shouldDelay */); + } } - private void initSuggest() { + private void resetSuggest() { final Locale switcherSubtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); final String switcherLocaleStr = switcherSubtypeLocale.toString(); final Locale subtypeLocale; - final String localeStr; if (TextUtils.isEmpty(switcherLocaleStr)) { // This happens in very rare corner cases - for example, immediately after a switch // to LatinIME has been requested, about a frame later another switch happens. In this @@ -619,132 +628,70 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // of knowing anyway. Log.e(TAG, "System is reporting no current subtype."); subtypeLocale = getResources().getConfiguration().locale; - localeStr = subtypeLocale.toString(); } else { subtypeLocale = switcherSubtypeLocale; - localeStr = switcherLocaleStr; - } - - final Suggest newSuggest = new Suggest(this /* Context */, subtypeLocale, - this /* SuggestInitializationListener */); - final SettingsValues settingsValues = mSettings.getCurrent(); - if (settingsValues.mCorrectionEnabled) { - newSuggest.setAutoCorrectionThreshold(settingsValues.mAutoCorrectionThreshold); - } - - mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.getInstance().initSuggest(newSuggest); } - - mUserDictionary = new UserBinaryDictionary(this, localeStr); - mIsUserDictionaryAvailable = mUserDictionary.isEnabled(); - newSuggest.setUserDictionary(mUserDictionary); - - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - - mUserHistoryDictionary = PersonalizationHelper.getUserHistoryDictionary( - this, localeStr, prefs); - newSuggest.setUserHistoryDictionary(mUserHistoryDictionary); - mPersonalizationDictionary = PersonalizationHelper - .getPersonalizationDictionary(this, localeStr, prefs); - newSuggest.setPersonalizationDictionary(mPersonalizationDictionary); - mPersonalizationPredictionDictionary = PersonalizationHelper - .getPersonalizationPredictionDictionary(this, localeStr, prefs); - newSuggest.setPersonalizationPredictionDictionary(mPersonalizationPredictionDictionary); - - final Suggest oldSuggest = mSuggest; - resetContactsDictionary(null != oldSuggest ? oldSuggest.getContactsDictionary() : null); - mSuggest = newSuggest; - if (oldSuggest != null) oldSuggest.close(); + resetSuggestForLocale(subtypeLocale); } /** - * Resets the contacts dictionary in mSuggest according to the user settings. + * Reset suggest by loading dictionaries for the locale and the current settings values. * - * 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 + * @param locale the locale */ - private void resetContactsDictionary(final ContactsBinaryDictionary oldContactsDictionary) { - final Suggest suggest = mSuggest; - final boolean shouldSetDictionary = - (null != suggest && mSettings.getCurrent().mUseContactsDict); - - final ContactsBinaryDictionary dictionaryToUse; - if (!shouldSetDictionary) { - // Make sure the dictionary is closed. If it is already closed, this is a no-op, - // so it's safe to call it anyways. - if (null != oldContactsDictionary) oldContactsDictionary.close(); - dictionaryToUse = null; - } else { - final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale(); - if (null != oldContactsDictionary) { - if (!oldContactsDictionary.mLocale.equals(locale)) { - // If the locale has changed then recreate the contacts dictionary. This - // allows locale dependent rules for handling bigram name predictions. - oldContactsDictionary.close(); - dictionaryToUse = new ContactsBinaryDictionary(this, locale); - } else { - // Make sure the old contacts dictionary is opened. If it is already open, - // this is a no-op, so it's safe to call it anyways. - oldContactsDictionary.reopen(this); - dictionaryToUse = oldContactsDictionary; - } - } else { - dictionaryToUse = new ContactsBinaryDictionary(this, locale); - } - } - - if (null != suggest) { - suggest.setContactsDictionary(dictionaryToUse); + private void resetSuggestForLocale(final Locale locale) { + final SettingsValues settingsValues = mSettings.getCurrent(); + mDictionaryFacilitator.resetDictionaries(this /* context */, locale, + settingsValues.mUseContactsDict, settingsValues.mUsePersonalizedDicts, + false /* forceReloadMainDictionary */, this); + if (settingsValues.mAutoCorrectionEnabled) { + mInputLogic.mSuggest.setAutoCorrectionThreshold( + settingsValues.mAutoCorrectionThreshold); } } + /** + * Reset suggest by loading the main dictionary of the current locale. + */ /* package private */ void resetSuggestMainDict() { - final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); - mSuggest.resetMainDict(this, subtypeLocale, this /* SuggestInitializationListener */); - mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale); + final SettingsValues settingsValues = mSettings.getCurrent(); + mDictionaryFacilitator.resetDictionaries(this /* context */, + mDictionaryFacilitator.getLocale(), settingsValues.mUseContactsDict, + settingsValues.mUsePersonalizedDicts, true /* forceReloadMainDictionary */, this); } @Override public void onDestroy() { - final Suggest suggest = mSuggest; - if (suggest != null) { - suggest.close(); - mSuggest = null; - } + mDictionaryFacilitator.closeDictionaries(); + mPersonalizationDictionaryUpdater.onDestroy(); + mContextualDictionaryUpdater.onDestroy(); mSettings.onDestroy(); - unregisterReceiver(mReceiver); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.getInstance().onDestroy(); - } + unregisterReceiver(mConnectivityAndRingerModeChangeReceiver); unregisterReceiver(mDictionaryPackInstallReceiver); - PersonalizationDictionarySessionRegister.onDestroy(this); - LatinImeLogger.commit(); - LatinImeLogger.onDestroy(); - if (mInputUpdater != null) { - mInputUpdater.quitLooper(); - } + unregisterReceiver(mDictionaryDumpBroadcastReceiver); + StatsUtils.onDestroy(); super.onDestroy(); } + @UsedForTesting + public void recycle() { + unregisterReceiver(mDictionaryPackInstallReceiver); + unregisterReceiver(mDictionaryDumpBroadcastReceiver); + unregisterReceiver(mConnectivityAndRingerModeChangeReceiver); + mInputLogic.recycle(); + } + @Override public void onConfigurationChanged(final Configuration conf) { - // If orientation changed while predicting, commit the change - if (mDisplayOrientation != conf.orientation) { - mDisplayOrientation = conf.orientation; + final SettingsValues settingsValues = mSettings.getCurrent(); + if (settingsValues.mDisplayOrientation != conf.orientation) { mHandler.startOrientationChanging(); - mConnection.beginBatchEdit(); - commitTyped(LastComposedWord.NOT_A_SEPARATOR); - mConnection.finishComposingText(); - mConnection.endBatchEdit(); - if (isShowingOptionDialog()) { - mOptionsDialog.dismiss(); - } + mInputLogic.onOrientationChange(mSettings.getCurrent()); + } + // TODO: Remove this test. + if (!conf.locale.equals(mPersonalizationDictionaryUpdater.getLocale())) { + refreshPersonalizationDictionarySession(settingsValues); } - PersonalizationDictionarySessionRegister.onConfigurationChanged(this, conf); super.onConfigurationChanged(conf); } @@ -760,10 +707,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen .findViewById(android.R.id.extractArea); mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing); mSuggestionStripView = (SuggestionStripView)view.findViewById(R.id.suggestion_strip_view); - if (mSuggestionStripView != null) + if (hasSuggestionStripView()) { mSuggestionStripView.setListener(this, view); - if (LatinImeLogger.sVISUALDEBUG) { - mKeyPreviewBackingView.setBackgroundColor(0x10FF0000); } } @@ -798,6 +743,7 @@ 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. mSubtypeSwitcher.onSubtypeChanged(subtype); + mInputLogic.onSubtypeChanged(SubtypeLocaleUtils.getCombiningRulesExtraValue(subtype)); loadKeyboard(); } @@ -834,30 +780,18 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen + ", word caps = " + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0)); } - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - ResearchLogger.latinIME_onStartInputViewInternal(editorInfo, prefs); - } + Log.i(TAG, "Starting input. Cursor position = " + + editorInfo.initialSelStart + "," + editorInfo.initialSelEnd); + // TODO: Consolidate these checks with {@link InputAttributes}. if (InputAttributes.inPrivateImeOptions(null, NO_MICROPHONE_COMPAT, editorInfo)) { - Log.w(TAG, "Deprecated private IME option specified: " - + editorInfo.privateImeOptions); + Log.w(TAG, "Deprecated private IME option specified: " + editorInfo.privateImeOptions); Log.w(TAG, "Use " + getPackageName() + "." + NO_MICROPHONE + " instead"); } if (InputAttributes.inPrivateImeOptions(getPackageName(), FORCE_ASCII, editorInfo)) { - Log.w(TAG, "Deprecated private IME option specified: " - + editorInfo.privateImeOptions); + Log.w(TAG, "Deprecated private IME option specified: " + editorInfo.privateImeOptions); Log.w(TAG, "Use EditorInfo.IME_FLAG_FORCE_ASCII flag instead"); } - final PackageInfo packageInfo = - TargetPackageInfoGetterTask.getCachedPackageInfo(editorInfo.packageName); - mAppWorkAroundsUtils.setPackageInfo(packageInfo); - if (null == packageInfo) { - new TargetPackageInfoGetterTask(this /* context */, this /* listener */) - .execute(editorInfo.packageName); - } - - LatinImeLogger.onStartInputView(editorInfo); // In landscape mode, this method gets called without the input view being created. if (mainKeyboardView == null) { return; @@ -878,57 +812,57 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // The EditorInfo might have a flag that affects fullscreen mode. // Note: This call should be done by InputMethodService? updateFullscreenMode(); - mApplicationSpecifiedCompletions = null; // The app calling setText() has the effect of clearing the composing // span, so we should reset our state unconditionally, even if restarting is true. - mEnteredText = null; - resetComposingState(true /* alsoResetLastComposedWord */); - mDeleteCount = 0; - mSpaceState = SPACE_STATE_NONE; - mRecapitalizeStatus.deactivate(); - mCurrentlyPressedHardwareKeys.clear(); + // We also tell the input logic about the combining rules for the current subtype, so + // it can adjust its combiners if needed. + mInputLogic.startInput(mSubtypeSwitcher.getCombiningRulesExtraValueOfCurrentSubtype()); // Note: the following does a round-trip IPC on the main thread: be careful final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); - final Suggest suggest = mSuggest; - if (null != suggest && null != currentLocale && !currentLocale.equals(suggest.mLocale)) { - initSuggest(); - } - if (mSuggestionStripView != null) { - // This will set the punctuation suggestions if next word suggestion is off; - // otherwise it will clear the suggestion strip. - setPunctuationSuggestions(); + final Suggest suggest = mInputLogic.mSuggest; + if (null != currentLocale && !currentLocale.equals(suggest.getLocale())) { + // TODO: Do this automatically. + resetSuggest(); } - mSuggestedWords = SuggestedWords.EMPTY; - // Sometimes, while rotating, for some reason the framework tells the app we are not - // connected to it and that means we can't refresh the cache. In this case, schedule a - // refresh later. + // TODO[IL]: Can the following be moved to InputLogic#startInput? final boolean canReachInputConnection; - if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(editorInfo.initialSelStart, + if (!mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess( + editorInfo.initialSelStart, editorInfo.initialSelEnd, false /* shouldFinishComposition */)) { + // Sometimes, while rotating, for some reason the framework tells the app we are not + // connected to it and that means we can't refresh the cache. In this case, schedule a + // refresh later. // We try resetting the caches up to 5 times before giving up. mHandler.postResetCaches(isDifferentTextField, 5 /* remainingTries */); // mLastSelection{Start,End} are reset later in this method, don't need to do it here canReachInputConnection = false; } else { - if (isDifferentTextField) { - mHandler.postResumeSuggestions(); - } + // When rotating, initialSelStart and initialSelEnd sometimes are lying. Make a best + // effort to work around this bug. + mInputLogic.mConnection.tryFixLyingCursorPosition(); + mHandler.postResumeSuggestions(true /* shouldIncludeResumedWordInSuggestions */, + true /* shouldDelay */); canReachInputConnection = true; } + if (isDifferentTextField || + !currentSettingsValues.hasSameOrientation(getResources().getConfiguration())) { + loadSettings(); + } if (isDifferentTextField) { mainKeyboardView.closing(); - loadSettings(); currentSettingsValues = mSettings.getCurrent(); - if (suggest != null && currentSettingsValues.mCorrectionEnabled) { - suggest.setAutoCorrectionThreshold(currentSettingsValues.mAutoCorrectionThreshold); + if (currentSettingsValues.mAutoCorrectionEnabled) { + suggest.setAutoCorrectionThreshold( + currentSettingsValues.mAutoCorrectionThreshold); } - switcher.loadKeyboard(editorInfo, currentSettingsValues); + switcher.loadKeyboard(editorInfo, currentSettingsValues, getCurrentAutoCapsState(), + getCurrentRecapitalizeState()); if (!canReachInputConnection) { // If we can't reach the input connection, we will call loadKeyboard again later, // so we need to save its state now. The call will be done in #retryResetCaches. @@ -937,26 +871,23 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } else if (restarting) { // TODO: Come up with a more comprehensive way to reset the keyboard layout when // a keyboard layout set doesn't get reloaded in this method. - switcher.resetKeyboardStateToAlphabet(); + switcher.resetKeyboardStateToAlphabet(getCurrentAutoCapsState(), + getCurrentRecapitalizeState()); // In apps like Talk, we come here when the text is sent and the field gets emptied and // we need to re-evaluate the shift state, but not the whole layout which would be // disruptive. // Space state must be updated before calling updateShiftState - switcher.updateShiftState(); + switcher.requestUpdatingShiftState(getCurrentAutoCapsState(), + getCurrentRecapitalizeState()); } - setSuggestionStripShownInternal( - isSuggestionsStripVisible(), /* needsInputViewShown */ false); - - mLastSelectionStart = editorInfo.initialSelStart; - mLastSelectionEnd = editorInfo.initialSelEnd; - // In some cases (namely, after rotation of the device) editorInfo.initialSelStart is lying - // so we try using some heuristics to find out about these and fix them. - tryFixLyingCursorPosition(); + // This will set the punctuation suggestions if next word suggestion is off; + // otherwise it will clear the suggestion strip. + setNeutralSuggestionStrip(); mHandler.cancelUpdateSuggestionStrip(); - mHandler.cancelDoubleSpacePeriodTimer(); - mainKeyboardView.setMainDictionaryAvailability(mIsMainDictionaryAvailable); + mainKeyboardView.setMainDictionaryAvailability( + mDictionaryFacilitator.hasInitializedMainDictionary()); mainKeyboardView.setKeyPreviewPopupEnabled(currentSettingsValues.mKeyPreviewPopupOn, currentSettingsValues.mKeyPreviewPopupDismissDelay); mainKeyboardView.setSlidingKeyInputPreviewEnabled( @@ -966,76 +897,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen currentSettingsValues.mGestureTrailEnabled, currentSettingsValues.mGestureFloatingPreviewTextEnabled); - initPersonalizationDebugSettings(currentSettingsValues); - + // Contextual dictionary should be updated for the current application. + mContextualDictionaryUpdater.onStartInputView(editorInfo.packageName); if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); } - /** - * Try to get the text from the editor to expose lies the framework may have been - * telling us. Concretely, when the device rotates, the frameworks tells us about where the - * cursor used to be initially in the editor at the time it first received the focus; this - * may be completely different from the place it is upon rotation. Since we don't have any - * means to get the real value, try at least to ask the text view for some characters and - * detect the most damaging cases: when the cursor position is declared to be much smaller - * than it really is. - */ - private void tryFixLyingCursorPosition() { - final CharSequence textBeforeCursor = - mConnection.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0); - if (null == textBeforeCursor) { - mLastSelectionStart = mLastSelectionEnd = NOT_A_CURSOR_POSITION; - } else { - final int textLength = textBeforeCursor.length(); - if (textLength > mLastSelectionStart - || (textLength < Constants.EDITOR_CONTENTS_CACHE_SIZE - && mLastSelectionStart < Constants.EDITOR_CONTENTS_CACHE_SIZE)) { - // It should not be possible to have only one of those variables be - // NOT_A_CURSOR_POSITION, so if they are equal, either the selection is zero-sized - // (simple cursor, no selection) or there is no cursor/we don't know its pos - final boolean wasEqual = mLastSelectionStart == mLastSelectionEnd; - mLastSelectionStart = textLength; - // We can't figure out the value of mLastSelectionEnd :( - // But at least if it's smaller than mLastSelectionStart something is wrong, - // and if they used to be equal we also don't want to make it look like there is a - // selection. - if (wasEqual || mLastSelectionStart > mLastSelectionEnd) { - mLastSelectionEnd = mLastSelectionStart; - } - } - } - } - - // Initialization of personalization debug settings. This must be called inside - // onStartInputView. - private void initPersonalizationDebugSettings(SettingsValues currentSettingsValues) { - if (mUseOnlyPersonalizationDictionaryForDebug - != currentSettingsValues.mUseOnlyPersonalizationDictionaryForDebug) { - // Only for debug - initSuggest(); - mUseOnlyPersonalizationDictionaryForDebug = - currentSettingsValues.mUseOnlyPersonalizationDictionaryForDebug; - } - - if (mBoostPersonalizationDictionaryForDebug != - currentSettingsValues.mBoostPersonalizationDictionaryForDebug) { - // Only for debug - mBoostPersonalizationDictionaryForDebug = - currentSettingsValues.mBoostPersonalizationDictionaryForDebug; - if (mBoostPersonalizationDictionaryForDebug) { - UserHistoryForgettingCurveUtils.boostMaxFreqForDebug(); - } else { - UserHistoryForgettingCurveUtils.resetMaxFreqForDebug(); - } - } - } - - // Callback for the TargetPackageInfoGetterTask - @Override - public void onTargetPackageInfoKnown(final PackageInfo info) { - mAppWorkAroundsUtils.setPackageInfo(info); - } - @Override public void onWindowHidden() { super.onWindowHidden(); @@ -1048,7 +914,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private void onFinishInputInternal() { super.onFinishInput(); - LatinImeLogger.commit(); final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView != null) { mainKeyboardView.closing(); @@ -1057,18 +922,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private void onFinishInputViewInternal(final boolean finishingInput) { super.onFinishInputView(finishingInput); - mKeyboardSwitcher.onFinishInputView(); mKeyboardSwitcher.deallocateMemory(); // Remove pending messages related to update suggestions mHandler.cancelUpdateSuggestionStrip(); // Should do the following in onFinishInputInternal but until JB MR2 it's not called :( - if (mWordComposer.isComposingWord()) mConnection.finishComposingText(); - resetComposingState(true /* alsoResetLastComposedWord */); - // Notify ResearchLogger - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_onFinishInputViewInternal(finishingInput, mLastSelectionStart, - mLastSelectionEnd, getCurrentInputConnection()); - } + mInputLogic.finishInput(); } @Override @@ -1078,102 +936,29 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart, composingSpanEnd); if (DEBUG) { - Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart - + ", ose=" + oldSelEnd - + ", lss=" + mLastSelectionStart - + ", lse=" + mLastSelectionEnd - + ", nss=" + newSelStart - + ", nse=" + newSelEnd - + ", cs=" + composingSpanStart - + ", ce=" + composingSpanEnd); - } - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - final boolean expectingUpdateSelectionFromLogger = - ResearchLogger.getAndClearLatinIMEExpectingUpdateSelection(); - ResearchLogger.latinIME_onUpdateSelection(mLastSelectionStart, mLastSelectionEnd, - oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart, - composingSpanEnd, mExpectingUpdateSelection, - expectingUpdateSelectionFromLogger, mConnection); - if (expectingUpdateSelectionFromLogger) { - // TODO: Investigate. Quitting now sounds wrong - we won't do the resetting work - return; - } + Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart + ", ose=" + oldSelEnd + + ", nss=" + newSelStart + ", nse=" + newSelEnd + + ", cs=" + composingSpanStart + ", ce=" + composingSpanEnd); } - final boolean selectionChanged = mLastSelectionStart != newSelStart - || mLastSelectionEnd != newSelEnd; - - // if composingSpanStart and composingSpanEnd are -1, it means there is no composing - // span in the view - we can use that to narrow down whether the cursor was moved - // by us or not. If we are composing a word but there is no composing span, then - // we know for sure the cursor moved while we were composing and we should reset - // the state. TODO: rescind this policy: the framework never removes the composing - // span on its own accord while editing. This test is useless. - final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1; - // If the keyboard is not visible, we don't need to do all the housekeeping work, as it // will be reset when the keyboard shows up anyway. // TODO: revisit this when LatinIME supports hardware keyboards. // NOTE: the test harness subclasses LatinIME and overrides isInputViewShown(). // TODO: find a better way to simulate actual execution. - if (isInputViewShown() && !mExpectingUpdateSelection - && !mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart)) { - // TAKE CARE: there is a race condition when we enter this test even when the user - // did not explicitly move the cursor. This happens when typing fast, where two keys - // turn this flag on in succession and both onUpdateSelection() calls arrive after - // the second one - the first call successfully avoids this test, but the second one - // enters. For the moment we rely on noComposingSpan to further reduce the impact. - - // TODO: the following is probably better done in resetEntireInputState(). - // it should only happen when the cursor moved, and the very purpose of the - // test below is to narrow down whether this happened or not. Likewise with - // the call to updateShiftState. - // We set this to NONE because after a cursor move, we don't want the space - // state-related special processing to kick in. - mSpaceState = SPACE_STATE_NONE; - - // TODO: is it still necessary to test for composingSpan related stuff? - final boolean selectionChangedOrSafeToReset = selectionChanged - || (!mWordComposer.isComposingWord()) || noComposingSpan; - final boolean hasOrHadSelection = (oldSelStart != oldSelEnd - || newSelStart != newSelEnd); - final int moveAmount = newSelStart - oldSelStart; - if (selectionChangedOrSafeToReset && (hasOrHadSelection - || !mWordComposer.moveCursorByAndReturnIfInsideComposingWord(moveAmount))) { - // If we are composing a word and moving the cursor, we would want to set a - // suggestion span for recorrection to work correctly. Unfortunately, that - // would involve the keyboard committing some new text, which would move the - // cursor back to where it was. Latin IME could then fix the position of the cursor - // again, but the asynchronous nature of the calls results in this wreaking havoc - // with selection on double tap and the like. - // Another option would be to send suggestions each time we set the composing - // text, but that is probably too expensive to do, so we decided to leave things - // as is. - resetEntireInputState(newSelStart); - } else { - // resetEntireInputState calls resetCachesUponCursorMove, but with the second - // argument as true. But in all cases where we don't reset the entire input state, - // we still want to tell the rich input connection about the new cursor position so - // that it can update its caches. - mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart, - false /* shouldFinishComposition */); - } - - // We moved the cursor. If we are touching a word, we need to resume suggestion, - // unless suggestions are off. - if (isSuggestionsStripVisible()) { - mHandler.postResumeSuggestions(); - } - // Reset the last recapitalization. - mRecapitalizeStatus.deactivate(); - mKeyboardSwitcher.updateShiftState(); + if (isInputViewShown() && + mInputLogic.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd)) { + mKeyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(), + getCurrentRecapitalizeState()); } - mExpectingUpdateSelection = false; + } - // Make a note of the cursor position - mLastSelectionStart = newSelStart; - mLastSelectionEnd = newSelEnd; - mSubtypeState.currentSubtypeUsed(); + @Override + public void onUpdateCursor(final Rect rect) { + if (DEBUG) { + Log.i(TAG, "onUpdateCursor:" + rect.toShortString()); + } + super.onUpdateCursor(rect); } /** @@ -1186,7 +971,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen */ @Override public void onExtractedTextClicked() { - if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) return; + if (mSettings.getCurrent().isSuggestionsRequested()) { + return; + } super.onExtractedTextClicked(); } @@ -1202,22 +989,19 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen */ @Override public void onExtractedCursorMovement(final int dx, final int dy) { - if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) return; + if (mSettings.getCurrent().isSuggestionsRequested()) { + return; + } super.onExtractedCursorMovement(dx, dy); } @Override public void hideWindow() { - LatinImeLogger.commit(); mKeyboardSwitcher.onHideWindow(); - if (AccessibilityUtils.getInstance().isAccessibilityEnabled()) { - AccessibleKeyboardViewProxy.getInstance().onHideWindow(); - } - if (TRACE) Debug.stopMethodTracing(); - if (mOptionsDialog != null && mOptionsDialog.isShowing()) { + if (isShowingOptionDialog()) { mOptionsDialog.dismiss(); mOptionsDialog = null; } @@ -1234,56 +1018,25 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } } - if (!mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()) return; + if (!mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()) { + return; + } + // If we have an update request in flight, we need to cancel it so it does not override + // these completions. + mHandler.cancelUpdateSuggestionStrip(); if (applicationSpecifiedCompletions == null) { - clearSuggestionStrip(); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_onDisplayCompletions(null); - } + setNeutralSuggestionStrip(); return; } - mApplicationSpecifiedCompletions = - CompletionInfoUtils.removeNulls(applicationSpecifiedCompletions); final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords = SuggestedWords.getFromApplicationSpecifiedCompletions( applicationSpecifiedCompletions); - final SuggestedWords suggestedWords = new SuggestedWords( - applicationSuggestedWords, - false /* typedWordValid */, - false /* hasAutoCorrectionCandidate */, - false /* isPunctuationSuggestions */, - false /* isObsoleteSuggestions */, - false /* isPrediction */); - // When in fullscreen mode, show completions generated by the application - final boolean isAutoCorrection = false; - setSuggestedWords(suggestedWords, isAutoCorrection); - setAutoCorrectionIndicator(isAutoCorrection); - setSuggestionStripShown(true); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions); - } - } - - private void setSuggestionStripShownInternal(final boolean shown, - final boolean needsInputViewShown) { - // TODO: Modify this if we support suggestions with hard keyboard - if (onEvaluateInputViewShown() && mSuggestionStripView != null) { - final boolean inputViewShown = mKeyboardSwitcher.isShowingMainKeyboardOrEmojiPalettes(); - final boolean shouldShowSuggestions = shown - && (needsInputViewShown ? inputViewShown : true); - if (isFullscreenMode()) { - mSuggestionStripView.setVisibility( - shouldShowSuggestions ? View.VISIBLE : View.GONE); - } else { - mSuggestionStripView.setVisibility( - shouldShowSuggestions ? View.VISIBLE : View.INVISIBLE); - } - } - } - - private void setSuggestionStripShown(final boolean shown) { - setSuggestionStripShownInternal(shown, /* needsInputViewShown */true); + final SuggestedWords suggestedWords = new SuggestedWords(applicationSuggestedWords, + null /* rawSuggestions */, false /* typedWordValid */, false /* willAutoCorrect */, + false /* isObsoleteSuggestions */, false /* isPrediction */); + // When in fullscreen mode, show completions generated by the application forcibly + setSuggestedWords(suggestedWords); } private int getAdjustedBackingViewHeight() { @@ -1317,7 +1070,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen public void onComputeInsets(final InputMethodService.Insets outInsets) { super.onComputeInsets(outInsets); final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView(); - if (visibleKeyboardView == null || mSuggestionStripView == null) { + if (visibleKeyboardView == null || !hasSuggestionStripView()) { return; } final int adjustedBackingHeight = getAdjustedBackingViewHeight(); @@ -1353,9 +1106,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public boolean onEvaluateFullscreenMode() { - // Reread resource value here, because this method is called by framework anytime as needed. - final boolean isFullscreenModeAllowed = - Settings.readUseFullscreenMode(getResources()); + // Reread resource value here, because this method is called by the framework as needed. + final boolean isFullscreenModeAllowed = Settings.readUseFullscreenMode(getResources()); if (super.onEvaluateFullscreenMode() && isFullscreenModeAllowed) { // TODO: Remove this hack. Actually we should not really assume NO_EXTRACT_UI // implies NO_FULLSCREEN. However, the framework mistakenly does. i.e. NO_EXTRACT_UI @@ -1378,138 +1130,30 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mKeyPreviewBackingView.setVisibility(isFullscreenMode() ? View.GONE : View.VISIBLE); } - // This will reset the whole input state to the starting state. It will clear - // the composing word, reset the last composed word, tell the inputconnection about it. - private void resetEntireInputState(final int newCursorPosition) { - final boolean shouldFinishComposition = mWordComposer.isComposingWord(); - resetComposingState(true /* alsoResetLastComposedWord */); - final SettingsValues settingsValues = mSettings.getCurrent(); - if (settingsValues.mBigramPredictionEnabled) { - clearSuggestionStrip(); - } else { - setSuggestedWords(settingsValues.mSuggestPuncList, false); - } - mConnection.resetCachesUponCursorMoveAndReturnSuccess(newCursorPosition, - shouldFinishComposition); + private int getCurrentAutoCapsState() { + return mInputLogic.getCurrentAutoCapsState(mSettings.getCurrent()); } - private void resetComposingState(final boolean alsoResetLastComposedWord) { - mWordComposer.reset(); - if (alsoResetLastComposedWord) - mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; + private int getCurrentRecapitalizeState() { + return mInputLogic.getCurrentRecapitalizeState(); } - private void commitTyped(final String separatorString) { - if (!mWordComposer.isComposingWord()) return; - final String typedWord = mWordComposer.getTypedWord(); - if (typedWord.length() > 0) { - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.getInstance().onWordFinished(typedWord, mWordComposer.isBatchMode()); - } - commitChosenWord(typedWord, LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, - separatorString); - } - } - - // Called from the KeyboardSwitcher which needs to know auto caps state to display - // the right layout. - public int getCurrentAutoCapsState() { - final SettingsValues currentSettingsValues = mSettings.getCurrent(); - if (!currentSettingsValues.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF; - - final EditorInfo ei = getCurrentInputEditorInfo(); - if (ei == null) return Constants.TextUtils.CAP_MODE_OFF; - final int inputType = ei.inputType; - // Warning: this depends on mSpaceState, which may not be the most current value. If - // mSpaceState gets updated later, whoever called this may need to be told about it. - return mConnection.getCursorCapsMode(inputType, currentSettingsValues, - SPACE_STATE_PHANTOM == mSpaceState); - } - - public int getCurrentRecapitalizeState() { - if (!mRecapitalizeStatus.isActive() - || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) { - // Not recapitalizing at the moment - return RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE; - } - return mRecapitalizeStatus.getCurrentMode(); - } - - // Factor in auto-caps and manual caps and compute the current caps mode. - private int getActualCapsMode() { - final int keyboardShiftMode = mKeyboardSwitcher.getKeyboardShiftMode(); - if (keyboardShiftMode != WordComposer.CAPS_MODE_AUTO_SHIFTED) return keyboardShiftMode; - final int auto = getCurrentAutoCapsState(); - if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) { - return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED; - } - if (0 != auto) return WordComposer.CAPS_MODE_AUTO_SHIFTED; - return WordComposer.CAPS_MODE_OFF; - } - - private void swapSwapperAndSpace() { - final CharSequence lastTwo = mConnection.getTextBeforeCursor(2, 0); - // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called. - if (lastTwo != null && lastTwo.length() == 2 - && lastTwo.charAt(0) == Constants.CODE_SPACE) { - mConnection.deleteSurroundingText(2, 0); - final String text = lastTwo.charAt(1) + " "; - mConnection.commitText(text, 1); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_swapSwapperAndSpace(lastTwo, text); - } - mKeyboardSwitcher.updateShiftState(); - } + public Locale getCurrentSubtypeLocale() { + return mSubtypeSwitcher.getCurrentSubtypeLocale(); } - private boolean maybeDoubleSpacePeriod() { - final SettingsValues currentSettingsValues = mSettings.getCurrent(); - if (!currentSettingsValues.mUseDoubleSpacePeriod) return false; - if (!mHandler.isAcceptingDoubleSpacePeriod()) return false; - // We only do this when we see two spaces and an accepted code point before the cursor. - // The code point may be a surrogate pair but the two spaces may not, so we need 4 chars. - final CharSequence lastThree = mConnection.getTextBeforeCursor(4, 0); - if (null == lastThree) return false; - final int length = lastThree.length(); - if (length < 3) return false; - if (lastThree.charAt(length - 1) != Constants.CODE_SPACE) return false; - if (lastThree.charAt(length - 2) != Constants.CODE_SPACE) return false; - // We know there are spaces in pos -1 and -2, and we have at least three chars. - // If we have only three chars, isSurrogatePairs can't return true as charAt(1) is a space, - // so this is fine. - final int firstCodePoint = - Character.isSurrogatePair(lastThree.charAt(0), lastThree.charAt(1)) ? - Character.codePointAt(lastThree, 0) : lastThree.charAt(length - 3); - if (canBeFollowedByDoubleSpacePeriod(firstCodePoint)) { - mHandler.cancelDoubleSpacePeriodTimer(); - mConnection.deleteSurroundingText(2, 0); - final String textToInsert = new String( - new int[] { currentSettingsValues.mSentenceSeparator, Constants.CODE_SPACE }, - 0, 2); - mConnection.commitText(textToInsert, 1); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_maybeDoubleSpacePeriod(textToInsert, - false /* isBatchMode */); - } - mKeyboardSwitcher.updateShiftState(); - return true; + /** + * @param codePoints code points to get coordinates for. + * @return x,y coordinates for this keyboard, as a flattened array. + */ + public int[] getCoordinatesForCurrentKeyboard(final int[] codePoints) { + final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); + if (null == keyboard) { + return CoordinateUtils.newCoordinateArray(codePoints.length, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); + } else { + return keyboard.getCoordinates(codePoints); } - return false; - } - - private static boolean canBeFollowedByDoubleSpacePeriod(final int codePoint) { - // TODO: Check again whether there really ain't a better way to check this. - // TODO: This should probably be language-dependant... - return Character.isLetterOrDigit(codePoint) - || codePoint == Constants.CODE_SINGLE_QUOTE - || codePoint == Constants.CODE_DOUBLE_QUOTE - || codePoint == Constants.CODE_CLOSING_PARENTHESIS - || codePoint == Constants.CODE_CLOSING_SQUARE_BRACKET - || codePoint == Constants.CODE_CLOSING_CURLY_BRACKET - || codePoint == Constants.CODE_CLOSING_ANGLE_BRACKET - || codePoint == Constants.CODE_PLUS - || codePoint == Constants.CODE_PERCENT - || Character.getType(codePoint) == Character.OTHER_SYMBOL; } // Callback for the {@link SuggestionStripView}, to call when the "add to dictionary" hint is @@ -1521,16 +1165,37 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return; } final String wordToEdit; - if (CapsModeUtils.isAutoCapsMode(mLastComposedWord.mCapitalizedMode)) { - wordToEdit = word.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale()); + if (CapsModeUtils.isAutoCapsMode(mInputLogic.mLastComposedWord.mCapitalizedMode)) { + wordToEdit = word.toLowerCase(getCurrentSubtypeLocale()); } else { wordToEdit = word; } - mUserDictionary.addWordToUserDictionary(wordToEdit); + mDictionaryFacilitator.addWordToUserDictionary(this /* context */, wordToEdit); + } + + // Callback for the {@link SuggestionStripView}, to call when the important notice strip is + // pressed. + @Override + public void showImportantNoticeContents() { + showOptionDialog(new ImportantNoticeDialog(this /* context */, this /* listener */)); + } + + // Implement {@link ImportantNoticeDialog.ImportantNoticeDialogListener} + @Override + public void onClickSettingsOfImportantNoticeDialog(final int nextVersion) { + launchSettings(); } - private void onSettingsKeyPressed() { - if (isShowingOptionDialog()) return; + // Implement {@link ImportantNoticeDialog.ImportantNoticeDialogListener} + @Override + public void onUserAcknowledgmentOfImportantNoticeDialog(final int nextVersion) { + setNeutralSuggestionStrip(); + } + + public void displaySettingsDialog() { + if (isShowingOptionDialog()) { + return; + } showSubtypeSelectorAndSettings(); } @@ -1552,408 +1217,105 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return mOptionsDialog != null && mOptionsDialog.isShowing(); } - private void performEditorAction(final int actionId) { - mConnection.performEditorAction(actionId); - } - // TODO: Revise the language switch key behavior to make it much smarter and more reasonable. - private void handleLanguageSwitchKey() { + public void switchToNextSubtype() { final IBinder token = getWindow().getWindow().getAttributes().token; - if (mSettings.getCurrent().mIncludesOtherImesInLanguageSwitchList) { + if (shouldSwitchToOtherInputMethods()) { mRichImm.switchToNextInputMethod(token, false /* onlyCurrentIme */); return; } mSubtypeState.switchSubtype(token, mRichImm); } - private void sendDownUpKeyEvent(final int code) { - final long eventTime = SystemClock.uptimeMillis(); - mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime, - KeyEvent.ACTION_DOWN, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, - KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); - mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime, - KeyEvent.ACTION_UP, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, - KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); - } - - private void sendKeyCodePoint(final int code) { - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_sendKeyCodePoint(code); - } - // TODO: Remove this special handling of digit letters. - // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}. - if (code >= '0' && code <= '9') { - sendDownUpKeyEvent(code - '0' + KeyEvent.KEYCODE_0); - return; - } - - if (Constants.CODE_ENTER == code && mAppWorkAroundsUtils.isBeforeJellyBean()) { - // Backward compatibility mode. Before Jelly bean, the keyboard would simulate - // a hardware keyboard event on pressing enter or delete. This is bad for many - // reasons (there are race conditions with commits) but some applications are - // relying on this behavior so we continue to support it for older apps. - sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER); - } else { - mConnection.commitText(StringUtils.newSingleCodePointString(code), 1); - } - } - // Implementation of {@link KeyboardActionListener}. @Override - public void onCodeInput(final int primaryCode, final int x, final int y) { - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_onCodeInput(primaryCode, x, y); - } - final long when = SystemClock.uptimeMillis(); - if (primaryCode != Constants.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) { - mDeleteCount = 0; - } - mLastKeyTime = when; - mConnection.beginBatchEdit(); - final KeyboardSwitcher switcher = mKeyboardSwitcher; - // The space state depends only on the last character pressed and its own previous - // state. Here, we revert the space state to neutral if the key is actually modifying - // the input contents (any non-shift key), which is what we should do for - // all inputs that do not result in a special state. Each character handling is then - // free to override the state as they see fit. - final int spaceState = mSpaceState; - if (!mWordComposer.isComposingWord()) mIsAutoCorrectionIndicatorOn = false; - - // TODO: Consolidate the double-space period timer, mLastKeyTime, and the space state. - if (primaryCode != Constants.CODE_SPACE) { - mHandler.cancelDoubleSpacePeriodTimer(); - } - - boolean didAutoCorrect = false; - switch (primaryCode) { - case Constants.CODE_DELETE: - mSpaceState = SPACE_STATE_NONE; - handleBackspace(spaceState); - LatinImeLogger.logOnDelete(x, y); - break; - case Constants.CODE_SHIFT: - // Note: Calling back to the keyboard on Shift key is handled in - // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}. - final Keyboard currentKeyboard = switcher.getKeyboard(); + public void onCodeInput(final int codePoint, final int x, final int y, + final boolean isKeyRepeat) { + final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); + // x and y include some padding, but everything down the line (especially native + // code) needs the coordinates in the keyboard frame. + // TODO: We should reconsider which coordinate system should be used to represent + // keyboard event. Also we should pull this up -- LatinIME has no business doing + // this transformation, it should be done already before calling onCodeInput. + final int keyX = mainKeyboardView.getKeyX(x); + final int keyY = mainKeyboardView.getKeyY(y); + final int codeToSend; + if (Constants.CODE_SHIFT == codePoint) { + // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for + // alphabetic shift and shift while in symbol layout. + final Keyboard currentKeyboard = mKeyboardSwitcher.getKeyboard(); if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) { - // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for - // alphabetic shift and shift while in symbol layout. - handleRecapitalize(); - } - break; - case Constants.CODE_CAPSLOCK: - // Note: Changing keyboard to shift lock state is handled in - // {@link KeyboardSwitcher#onCodeInput(int)}. - break; - case Constants.CODE_SWITCH_ALPHA_SYMBOL: - // Note: Calling back to the keyboard on symbol key is handled in - // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}. - break; - case Constants.CODE_SETTINGS: - onSettingsKeyPressed(); - break; - case Constants.CODE_SHORTCUT: - mSubtypeSwitcher.switchToShortcutIME(this); - break; - case Constants.CODE_ACTION_NEXT: - performEditorAction(EditorInfo.IME_ACTION_NEXT); - break; - case Constants.CODE_ACTION_PREVIOUS: - performEditorAction(EditorInfo.IME_ACTION_PREVIOUS); - break; - case Constants.CODE_LANGUAGE_SWITCH: - handleLanguageSwitchKey(); - break; - case Constants.CODE_EMOJI: - // Note: Switching emoji keyboard is being handled in - // {@link KeyboardState#onCodeInput(int,int)}. - break; - case Constants.CODE_ENTER: - final EditorInfo editorInfo = getCurrentInputEditorInfo(); - final int imeOptionsActionId = - InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo); - if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) { - // Either we have an actionLabel and we should performEditorAction with actionId - // regardless of its value. - performEditorAction(editorInfo.actionId); - } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) { - // We didn't have an actionLabel, but we had another action to execute. - // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast, - // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it - // means there should be an action and the app didn't bother to set a specific - // code for it - presumably it only handles one. It does not have to be treated - // in any specific way: anything that is not IME_ACTION_NONE should be sent to - // performEditorAction. - performEditorAction(imeOptionsActionId); + codeToSend = codePoint; } else { - // No action label, and the action from imeOptions is NONE: this is a regular - // enter key that should input a carriage return. - didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState); + codeToSend = Constants.CODE_SYMBOL_SHIFT; } - break; - case Constants.CODE_SHIFT_ENTER: - didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState); - break; - default: - didAutoCorrect = handleNonSpecialCharacter(primaryCode, x, y, spaceState); - break; - } - switcher.onCodeInput(primaryCode); - // Reset after any single keystroke, except shift, capslock, and symbol-shift - if (!didAutoCorrect && primaryCode != Constants.CODE_SHIFT - && primaryCode != Constants.CODE_CAPSLOCK - && primaryCode != Constants.CODE_SWITCH_ALPHA_SYMBOL) - mLastComposedWord.deactivate(); - if (Constants.CODE_DELETE != primaryCode) { - mEnteredText = null; + } else { + codeToSend = codePoint; } - mConnection.endBatchEdit(); - } - - private boolean handleNonSpecialCharacter(final int primaryCode, final int x, final int y, - final int spaceState) { - mSpaceState = SPACE_STATE_NONE; - final boolean didAutoCorrect; - final SettingsValues settingsValues = mSettings.getCurrent(); - if (settingsValues.isWordSeparator(primaryCode) - || Character.getType(primaryCode) == Character.OTHER_SYMBOL) { - didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState); + if (Constants.CODE_SHORTCUT == codePoint) { + mSubtypeSwitcher.switchToShortcutIME(this); + // Still call the *#onCodeInput methods for readability. + } + final Event event = createSoftwareKeypressEvent(codeToSend, keyX, keyY, isKeyRepeat); + final InputTransaction completeInputTransaction = + mInputLogic.onCodeInput(mSettings.getCurrent(), event, + mKeyboardSwitcher.getKeyboardShiftMode(), + mKeyboardSwitcher.getCurrentKeyboardScriptId(), mHandler); + updateStateAfterInputTransaction(completeInputTransaction); + mKeyboardSwitcher.onCodeInput(codePoint, getCurrentAutoCapsState(), + getCurrentRecapitalizeState()); + } + + // A helper method to split the code point and the key code. Ultimately, they should not be + // squashed into the same variable, and this method should be removed. + private static Event createSoftwareKeypressEvent(final int keyCodeOrCodePoint, final int keyX, + final int keyY, final boolean isKeyRepeat) { + final int keyCode; + final int codePoint; + if (keyCodeOrCodePoint <= 0) { + keyCode = keyCodeOrCodePoint; + codePoint = Event.NOT_A_CODE_POINT; } else { - didAutoCorrect = false; - if (SPACE_STATE_PHANTOM == spaceState) { - if (settingsValues.mIsInternal) { - if (mWordComposer.isComposingWord() && mWordComposer.isBatchMode()) { - LatinImeLoggerUtils.onAutoCorrection( - "", mWordComposer.getTypedWord(), " ", mWordComposer); - } - } - if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { - // If we are in the middle of a recorrection, we need to commit the recorrection - // first so that we can insert the character at the current cursor position. - resetEntireInputState(mLastSelectionStart); - } else { - commitTyped(LastComposedWord.NOT_A_SEPARATOR); - } - } - final int keyX, keyY; - final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); - if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) { - keyX = x; - keyY = y; - } else { - keyX = Constants.NOT_A_COORDINATE; - keyY = Constants.NOT_A_COORDINATE; - } - handleCharacter(primaryCode, keyX, keyY, spaceState); + keyCode = Event.NOT_A_KEY_CODE; + codePoint = keyCodeOrCodePoint; } - mExpectingUpdateSelection = true; - return didAutoCorrect; + return Event.createSoftwareKeypressEvent(codePoint, keyCode, keyX, keyY, isKeyRepeat); } // Called from PointerTracker through the KeyboardActionListener interface @Override public void onTextInput(final String rawText) { - mConnection.beginBatchEdit(); - if (mWordComposer.isComposingWord()) { - commitCurrentAutoCorrection(rawText); - } else { - resetComposingState(true /* alsoResetLastComposedWord */); - } - mHandler.postUpdateSuggestionStrip(); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS - && ResearchLogger.RESEARCH_KEY_OUTPUT_TEXT.equals(rawText)) { - ResearchLogger.getInstance().onResearchKeySelected(this); - return; - } - final String text = specificTldProcessingOnTextInput(rawText); - if (SPACE_STATE_PHANTOM == mSpaceState) { - promotePhantomSpace(); - } - mConnection.commitText(text, 1); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_onTextInput(text, false /* isBatchMode */); - } - mConnection.endBatchEdit(); - // Space state must be updated before calling updateShiftState - mSpaceState = SPACE_STATE_NONE; - mKeyboardSwitcher.updateShiftState(); - mKeyboardSwitcher.onCodeInput(Constants.CODE_OUTPUT_TEXT); - mEnteredText = text; + // TODO: have the keyboard pass the correct key code when we need it. + final Event event = Event.createSoftwareTextEvent(rawText, Event.NOT_A_KEY_CODE); + final InputTransaction completeInputTransaction = + mInputLogic.onTextInput(mSettings.getCurrent(), event, + mKeyboardSwitcher.getKeyboardShiftMode(), mHandler); + updateStateAfterInputTransaction(completeInputTransaction); + mKeyboardSwitcher.onCodeInput(Constants.CODE_OUTPUT_TEXT, getCurrentAutoCapsState(), + getCurrentRecapitalizeState()); } @Override public void onStartBatchInput() { - mInputUpdater.onStartBatchInput(); - mHandler.cancelUpdateSuggestionStrip(); - mConnection.beginBatchEdit(); - final SettingsValues settingsValues = mSettings.getCurrent(); - if (mWordComposer.isComposingWord()) { - if (settingsValues.mIsInternal) { - if (mWordComposer.isBatchMode()) { - LatinImeLoggerUtils.onAutoCorrection( - "", mWordComposer.getTypedWord(), " ", mWordComposer); - } - } - final int wordComposerSize = mWordComposer.size(); - // Since isComposingWord() is true, the size is at least 1. - if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { - // If we are in the middle of a recorrection, we need to commit the recorrection - // first so that we can insert the batch input at the current cursor position. - resetEntireInputState(mLastSelectionStart); - } else if (wordComposerSize <= 1) { - // We auto-correct the previous (typed, not gestured) string iff it's one character - // long. The reason for this is, even in the middle of gesture typing, you'll still - // tap one-letter words and you want them auto-corrected (typically, "i" in English - // should become "I"). However for any longer word, we assume that the reason for - // tapping probably is that the word you intend to type is not in the dictionary, - // so we do not attempt to correct, on the assumption that if that was a dictionary - // word, the user would probably have gestured instead. - commitCurrentAutoCorrection(LastComposedWord.NOT_A_SEPARATOR); - } else { - commitTyped(LastComposedWord.NOT_A_SEPARATOR); - } - mExpectingUpdateSelection = true; - } - final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); - if (Character.isLetterOrDigit(codePointBeforeCursor) - || settingsValues.isUsuallyFollowedBySpace(codePointBeforeCursor)) { - mSpaceState = SPACE_STATE_PHANTOM; - } - mConnection.endBatchEdit(); - mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode()); + mInputLogic.onStartBatchInput(mSettings.getCurrent(), mKeyboardSwitcher, mHandler); } - static final class InputUpdater implements Handler.Callback { - private final Handler mHandler; - private final LatinIME mLatinIme; - private final Object mLock = new Object(); - private boolean mInBatchInput; // synchronized using {@link #mLock}. - - InputUpdater(final LatinIME latinIme) { - final HandlerThread handlerThread = new HandlerThread( - InputUpdater.class.getSimpleName()); - handlerThread.start(); - mHandler = new Handler(handlerThread.getLooper(), this); - mLatinIme = latinIme; - } - - private static final int MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 1; - private static final int MSG_GET_SUGGESTED_WORDS = 2; - - @Override - public boolean handleMessage(final Message msg) { - // TODO: straighten message passing - we don't need two kinds of messages calling - // each other. - switch (msg.what) { - case MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP: - updateBatchInput((InputPointers)msg.obj, msg.arg2 /* sequenceNumber */); - break; - case MSG_GET_SUGGESTED_WORDS: - mLatinIme.getSuggestedWords(msg.arg1 /* sessionId */, - msg.arg2 /* sequenceNumber */, (OnGetSuggestedWordsCallback) msg.obj); - break; - } - return true; - } - - // Run in the UI thread. - public void onStartBatchInput() { - synchronized (mLock) { - mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); - mInBatchInput = true; - mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( - SuggestedWords.EMPTY, false /* dismissGestureFloatingPreviewText */); - } - } - - // Run in the Handler thread. - private void updateBatchInput(final InputPointers batchPointers, final int sequenceNumber) { - synchronized (mLock) { - if (!mInBatchInput) { - // Batch input has ended or canceled while the message was being delivered. - return; - } - - getSuggestedWordsGestureLocked(batchPointers, sequenceNumber, - new OnGetSuggestedWordsCallback() { - @Override - public void onGetSuggestedWords(final SuggestedWords suggestedWords) { - mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( - suggestedWords, false /* dismissGestureFloatingPreviewText */); - } - }); - } - } - - // Run in the UI thread. - public void onUpdateBatchInput(final InputPointers batchPointers, - final int sequenceNumber) { - if (mHandler.hasMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP)) { - return; - } - mHandler.obtainMessage(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, 0 /* arg1 */, - sequenceNumber /* arg2 */, batchPointers /* obj */).sendToTarget(); - } - - public void onCancelBatchInput() { - synchronized (mLock) { - mInBatchInput = false; - mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( - SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */); - } - } - - // Run in the UI thread. - public void onEndBatchInput(final InputPointers batchPointers) { - synchronized(mLock) { - getSuggestedWordsGestureLocked(batchPointers, SuggestedWords.NOT_A_SEQUENCE_NUMBER, - new OnGetSuggestedWordsCallback() { - @Override - public void onGetSuggestedWords(final SuggestedWords suggestedWords) { - mInBatchInput = false; - mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(suggestedWords, - true /* dismissGestureFloatingPreviewText */); - mLatinIme.mHandler.onEndBatchInput(suggestedWords); - } - }); - } - } - - // {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to - // be synchronized. - private void getSuggestedWordsGestureLocked(final InputPointers batchPointers, - final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { - mLatinIme.mWordComposer.setBatchInputPointers(batchPointers); - mLatinIme.getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_GESTURE, - sequenceNumber, new OnGetSuggestedWordsCallback() { - @Override - public void onGetSuggestedWords(SuggestedWords suggestedWords) { - final int suggestionCount = suggestedWords.size(); - if (suggestionCount <= 1) { - final String mostProbableSuggestion = (suggestionCount == 0) ? null - : suggestedWords.getWord(0); - callback.onGetSuggestedWords( - mLatinIme.getOlderSuggestions(mostProbableSuggestion)); - } - callback.onGetSuggestedWords(suggestedWords); - } - }); - } + @Override + public void onUpdateBatchInput(final InputPointers batchPointers) { + mInputLogic.onUpdateBatchInput(mSettings.getCurrent(), batchPointers, mKeyboardSwitcher); + } - public void getSuggestedWords(final int sessionId, final int sequenceNumber, - final OnGetSuggestedWordsCallback callback) { - mHandler.obtainMessage(MSG_GET_SUGGESTED_WORDS, sessionId, sequenceNumber, callback) - .sendToTarget(); - } + @Override + public void onEndBatchInput(final InputPointers batchPointers) { + mInputLogic.onEndBatchInput(batchPointers); + } - void quitLooper() { - mHandler.removeMessages(MSG_GET_SUGGESTED_WORDS); - mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); - mHandler.getLooper().quit(); - } + @Override + public void onCancelBatchInput() { + mInputLogic.onCancelBatchInput(mHandler); } - // This method must run in UI Thread. + // This method must run on the UI Thread. private void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords, final boolean dismissGestureFloatingPreviewText) { showSuggestionStrip(suggestedWords); @@ -1964,107 +1326,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } - /* The sequence number member is only used in onUpdateBatchInput. It is increased each time - * auto-commit happens. The reason we need this is, when auto-commit happens we trim the - * input pointers that are held in a singleton, and to know how much to trim we rely on the - * results of the suggestion process that is held in mSuggestedWords. - * However, the suggestion process is asynchronous, and sometimes we may enter the - * onUpdateBatchInput method twice without having recomputed suggestions yet, or having - * received new suggestions generated from not-yet-trimmed input pointers. In this case, the - * mIndexOfTouchPointOfSecondWords member will be out of date, and we must not use it lest we - * remove an unrelated number of pointers (possibly even more than are left in the input - * pointers, leading to a crash). - * To avoid that, we increase the sequence number each time we auto-commit and trim the - * input pointers, and we do not use any suggested words that have been generated with an - * earlier sequence number. - */ - private int mAutoCommitSequenceNumber = 1; - @Override - public void onUpdateBatchInput(final InputPointers batchPointers) { - if (mSettings.getCurrent().mPhraseGestureEnabled) { - final SuggestedWordInfo candidate = mSuggestedWords.getAutoCommitCandidate(); - // If these suggested words have been generated with out of date input pointers, then - // we skip auto-commit (see comments above on the mSequenceNumber member). - if (null != candidate && mSuggestedWords.mSequenceNumber >= mAutoCommitSequenceNumber) { - if (candidate.mSourceDict.shouldAutoCommit(candidate)) { - final String[] commitParts = candidate.mWord.split(" ", 2); - batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord); - promotePhantomSpace(); - mConnection.commitText(commitParts[0], 0); - mSpaceState = SPACE_STATE_PHANTOM; - mKeyboardSwitcher.updateShiftState(); - mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode()); - ++mAutoCommitSequenceNumber; - } - } - } - mInputUpdater.onUpdateBatchInput(batchPointers, mAutoCommitSequenceNumber); - } - - // This method must run in UI Thread. - public void onEndBatchInputAsyncInternal(final SuggestedWords suggestedWords) { - final String batchInputText = suggestedWords.isEmpty() - ? null : suggestedWords.getWord(0); - if (TextUtils.isEmpty(batchInputText)) { - return; - } - mConnection.beginBatchEdit(); - if (SPACE_STATE_PHANTOM == mSpaceState) { - promotePhantomSpace(); - } - if (mSettings.getCurrent().mPhraseGestureEnabled) { - // Find the last space - final int indexOfLastSpace = batchInputText.lastIndexOf(Constants.CODE_SPACE) + 1; - if (0 != indexOfLastSpace) { - mConnection.commitText(batchInputText.substring(0, indexOfLastSpace), 1); - showSuggestionStrip(suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture()); - } - final String lastWord = batchInputText.substring(indexOfLastSpace); - mWordComposer.setBatchInputWord(lastWord); - mConnection.setComposingText(lastWord, 1); - } else { - mWordComposer.setBatchInputWord(batchInputText); - mConnection.setComposingText(batchInputText, 1); - } - mExpectingUpdateSelection = true; - mConnection.endBatchEdit(); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_onEndBatchInput(batchInputText, 0, suggestedWords); - } - // Space state must be updated before calling updateShiftState - mSpaceState = SPACE_STATE_PHANTOM; - mKeyboardSwitcher.updateShiftState(); - } - - @Override - public void onEndBatchInput(final InputPointers batchPointers) { - mInputUpdater.onEndBatchInput(batchPointers); - } - - private String specificTldProcessingOnTextInput(final String text) { - if (text.length() <= 1 || text.charAt(0) != Constants.CODE_PERIOD - || !Character.isLetter(text.charAt(1))) { - // Not a tld: do nothing. - return text; - } - // We have a TLD (or something that looks like this): make sure we don't add - // a space even if currently in phantom mode. - mSpaceState = SPACE_STATE_NONE; - // TODO: use getCodePointBeforeCursor instead to improve performance and simplify the code - final CharSequence lastOne = mConnection.getTextBeforeCursor(1, 0); - if (lastOne != null && lastOne.length() == 1 - && lastOne.charAt(0) == Constants.CODE_PERIOD) { - return text.substring(1); - } else { - return text; - } - } - // Called from PointerTracker through the KeyboardActionListener interface @Override public void onFinishSlidingInput() { // User finished sliding input. - mKeyboardSwitcher.onFinishSlidingInput(); + mKeyboardSwitcher.onFinishSlidingInput(getCurrentAutoCapsState(), + getCurrentRecapitalizeState()); } // Called from PointerTracker through the KeyboardActionListener interface @@ -2074,1006 +1341,126 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Nothing to do so far. } - @Override - public void onCancelBatchInput() { - mInputUpdater.onCancelBatchInput(); - } - - private void handleBackspace(final int spaceState) { - // We revert these in this method if the deletion doesn't happen. - mDeleteCount++; - mExpectingUpdateSelection = true; - - // In many cases, we may have to put the keyboard in auto-shift state again. However - // we want to wait a few milliseconds before doing it to avoid the keyboard flashing - // during key repeat. - mHandler.postUpdateShiftState(); - - if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { - // If we are in the middle of a recorrection, we need to commit the recorrection - // first so that we can remove the character at the current cursor position. - resetEntireInputState(mLastSelectionStart); - // When we exit this if-clause, mWordComposer.isComposingWord() will return false. - } - if (mWordComposer.isComposingWord()) { - if (mWordComposer.isBatchMode()) { - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - final String word = mWordComposer.getTypedWord(); - ResearchLogger.latinIME_handleBackspace_batch(word, 1); - } - final String rejectedSuggestion = mWordComposer.getTypedWord(); - mWordComposer.reset(); - mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion); - } else { - mWordComposer.deleteLast(); - } - mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); - mHandler.postUpdateSuggestionStrip(); - if (!mWordComposer.isComposingWord()) { - // If we just removed the last character, auto-caps mode may have changed so we - // need to re-evaluate. - mKeyboardSwitcher.updateShiftState(); - } - } else { - final SettingsValues currentSettings = mSettings.getCurrent(); - if (mLastComposedWord.canRevertCommit()) { - if (currentSettings.mIsInternal) { - LatinImeLoggerUtils.onAutoCorrectionCancellation(); - } - revertCommit(); - return; - } - if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) { - // Cancel multi-character input: remove the text we just entered. - // This is triggered on backspace after a key that inputs multiple characters, - // like the smiley key or the .com key. - mConnection.deleteSurroundingText(mEnteredText.length(), 0); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_handleBackspace_cancelTextInput(mEnteredText); - } - mEnteredText = null; - // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false. - // In addition we know that spaceState is false, and that we should not be - // reverting any autocorrect at this point. So we can safely return. - return; - } - if (SPACE_STATE_DOUBLE == spaceState) { - mHandler.cancelDoubleSpacePeriodTimer(); - if (mConnection.revertDoubleSpacePeriod()) { - // No need to reset mSpaceState, it has already be done (that's why we - // receive it as a parameter) - return; - } - } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) { - if (mConnection.revertSwapPunctuation()) { - // Likewise - return; - } - } - - // No cancelling of commit/double space/swap: we have a regular backspace. - // We should backspace one char and restart suggestion if at the end of a word. - if (mLastSelectionStart != mLastSelectionEnd) { - // If there is a selection, remove it. - final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart; - mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd); - // Reset mLastSelectionEnd to mLastSelectionStart. This is what is supposed to - // happen, and if it's wrong, the next call to onUpdateSelection will correct it, - // but we want to set it right away to avoid it being used with the wrong values - // later (typically, in a subsequent press on backspace). - mLastSelectionEnd = mLastSelectionStart; - mConnection.deleteSurroundingText(numCharsDeleted, 0); - } else { - // There is no selection, just delete one character. - if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) { - // This should never happen. - Log.e(TAG, "Backspace when we don't know the selection position"); - } - if (mAppWorkAroundsUtils.isBeforeJellyBean() || - currentSettings.mInputAttributes.isTypeNull()) { - // There are two possible reasons to send a key event: either the field has - // type TYPE_NULL, in which case the keyboard should send events, or we are - // running in backward compatibility mode. Before Jelly bean, the keyboard - // would simulate a hardware keyboard event on pressing enter or delete. This - // is bad for many reasons (there are race conditions with commits) but some - // applications are relying on this behavior so we continue to support it for - // older apps, so we retain this behavior if the app has target SDK < JellyBean. - sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); - if (mDeleteCount > DELETE_ACCELERATE_AT) { - sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); - } - } else { - final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); - if (codePointBeforeCursor == Constants.NOT_A_CODE) { - // Nothing to delete before the cursor. We have to revert the deletion - // states that were updated at the beginning of this method. - mDeleteCount--; - mExpectingUpdateSelection = false; - return; - } - final int lengthToDelete = - Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1; - mConnection.deleteSurroundingText(lengthToDelete, 0); - if (mDeleteCount > DELETE_ACCELERATE_AT) { - final int codePointBeforeCursorToDeleteAgain = - mConnection.getCodePointBeforeCursor(); - if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) { - final int lengthToDeleteAgain = Character.isSupplementaryCodePoint( - codePointBeforeCursorToDeleteAgain) ? 2 : 1; - mConnection.deleteSurroundingText(lengthToDeleteAgain, 0); - } - } - } - } - if (currentSettings.isSuggestionsRequested(mDisplayOrientation) - && currentSettings.mCurrentLanguageHasSpaces) { - restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(); - } - // We just removed a character. We need to update the auto-caps state. - mKeyboardSwitcher.updateShiftState(); - } - } - - /* - * Strip a trailing space if necessary and returns whether it's a swap weak space situation. - */ - private boolean maybeStripSpace(final int code, - final int spaceState, final boolean isFromSuggestionStrip) { - if (Constants.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) { - mConnection.removeTrailingSpace(); - return false; - } - if ((SPACE_STATE_WEAK == spaceState || SPACE_STATE_SWAP_PUNCTUATION == spaceState) - && isFromSuggestionStrip) { - final SettingsValues currentSettings = mSettings.getCurrent(); - if (currentSettings.isUsuallyPrecededBySpace(code)) return false; - if (currentSettings.isUsuallyFollowedBySpace(code)) return true; - mConnection.removeTrailingSpace(); - } - return false; - } - - private void handleCharacter(final int primaryCode, final int x, - final int y, final int spaceState) { - // TODO: refactor this method to stop flipping isComposingWord around all the time, and - // make it shorter (possibly cut into several pieces). Also factor handleNonSpecialCharacter - // which has the same name as other handle* methods but is not the same. - boolean isComposingWord = mWordComposer.isComposingWord(); - - // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead. - // See onStartBatchInput() to see how to do it. - final SettingsValues currentSettings = mSettings.getCurrent(); - if (SPACE_STATE_PHANTOM == spaceState && !currentSettings.isWordConnector(primaryCode)) { - if (isComposingWord) { - // Sanity check - throw new RuntimeException("Should not be composing here"); - } - promotePhantomSpace(); - } - - if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { - // If we are in the middle of a recorrection, we need to commit the recorrection - // first so that we can insert the character at the current cursor position. - resetEntireInputState(mLastSelectionStart); - isComposingWord = false; - } - // We want to find out whether to start composing a new word with this character. If so, - // we need to reset the composing state and switch isComposingWord. The order of the - // tests is important for good performance. - // We only start composing if we're not already composing. - if (!isComposingWord - // We only start composing if this is a word code point. Essentially that means it's a - // a letter or a word connector. - && currentSettings.isWordCodePoint(primaryCode) - // We never go into composing state if suggestions are not requested. - && currentSettings.isSuggestionsRequested(mDisplayOrientation) && - // In languages with spaces, we only start composing a word when we are not already - // touching a word. In languages without spaces, the above conditions are sufficient. - (!mConnection.isCursorTouchingWord(currentSettings) - || !currentSettings.mCurrentLanguageHasSpaces)) { - // Reset entirely the composing state anyway, then start composing a new word unless - // the character is a single quote or a dash. The idea here is, single quote and dash - // are not separators and they should be treated as normal characters, except in the - // first position where they should not start composing a word. - isComposingWord = (Constants.CODE_SINGLE_QUOTE != primaryCode - && Constants.CODE_DASH != primaryCode); - // Here we don't need to reset the last composed word. It will be reset - // when we commit this one, if we ever do; if on the other hand we backspace - // it entirely and resume suggestions on the previous word, we'd like to still - // have touch coordinates for it. - resetComposingState(false /* alsoResetLastComposedWord */); - } - if (isComposingWord) { - final int keyX, keyY; - if (Constants.isValidCoordinate(x) && Constants.isValidCoordinate(y)) { - final KeyDetector keyDetector = - mKeyboardSwitcher.getMainKeyboardView().getKeyDetector(); - keyX = keyDetector.getTouchX(x); - keyY = keyDetector.getTouchY(y); - } else { - keyX = x; - keyY = y; - } - mWordComposer.add(primaryCode, keyX, keyY); - // If it's the first letter, make note of auto-caps state - if (mWordComposer.size() == 1) { - mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode()); - } - mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); - } else { - final boolean swapWeakSpace = maybeStripSpace(primaryCode, - spaceState, Constants.SUGGESTION_STRIP_COORDINATE == x); - - sendKeyCodePoint(primaryCode); - - if (swapWeakSpace) { - swapSwapperAndSpace(); - mSpaceState = SPACE_STATE_WEAK; - } - // In case the "add to dictionary" hint was still displayed. - if (null != mSuggestionStripView) mSuggestionStripView.dismissAddToDictionaryHint(); - } - mHandler.postUpdateSuggestionStrip(); - if (currentSettings.mIsInternal) { - LatinImeLoggerUtils.onNonSeparator((char)primaryCode, x, y); - } - } - - private void handleRecapitalize() { - if (mLastSelectionStart == mLastSelectionEnd) return; // No selection - // If we have a recapitalize in progress, use it; otherwise, create a new one. - if (!mRecapitalizeStatus.isActive() - || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) { - final CharSequence selectedText = - mConnection.getSelectedText(0 /* flags, 0 for no styles */); - if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection - final SettingsValues currentSettings = mSettings.getCurrent(); - mRecapitalizeStatus.initialize(mLastSelectionStart, mLastSelectionEnd, - selectedText.toString(), currentSettings.mLocale, - currentSettings.mWordSeparators); - // We trim leading and trailing whitespace. - mRecapitalizeStatus.trim(); - // Trimming the object may have changed the length of the string, and we need to - // reposition the selection handles accordingly. As this result in an IPC call, - // only do it if it's actually necessary, in other words if the recapitalize status - // is not set at the same place as before. - if (!mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) { - mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart(); - mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd(); - } - } - mConnection.finishComposingText(); - mRecapitalizeStatus.rotate(); - final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart; - mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd); - mConnection.deleteSurroundingText(numCharsDeleted, 0); - mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0); - mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart(); - mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd(); - mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd); - // Match the keyboard to the new state. - mKeyboardSwitcher.updateShiftState(); - } - - // Returns true if we do an autocorrection, false otherwise. - private boolean handleSeparator(final int primaryCode, final int x, final int y, - final int spaceState) { - boolean didAutoCorrect = false; - final SettingsValues currentSettings = mSettings.getCurrent(); - // We avoid sending spaces in languages without spaces if we were composing. - final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == primaryCode - && !currentSettings.mCurrentLanguageHasSpaces && mWordComposer.isComposingWord(); - if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { - // If we are in the middle of a recorrection, we need to commit the recorrection - // first so that we can insert the separator at the current cursor position. - resetEntireInputState(mLastSelectionStart); - } - if (mWordComposer.isComposingWord()) { // May have changed since we stored wasComposing - if (currentSettings.mCorrectionEnabled) { - final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR - : StringUtils.newSingleCodePointString(primaryCode); - commitCurrentAutoCorrection(separator); - didAutoCorrect = true; - } else { - commitTyped(StringUtils.newSingleCodePointString(primaryCode)); - } - } - - final boolean swapWeakSpace = maybeStripSpace(primaryCode, spaceState, - Constants.SUGGESTION_STRIP_COORDINATE == x); - - if (SPACE_STATE_PHANTOM == spaceState && - currentSettings.isUsuallyPrecededBySpace(primaryCode)) { - promotePhantomSpace(); - } - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_handleSeparator(primaryCode, mWordComposer.isComposingWord()); - } - - if (!shouldAvoidSendingCode) { - sendKeyCodePoint(primaryCode); - } - - if (Constants.CODE_SPACE == primaryCode) { - if (currentSettings.isSuggestionsRequested(mDisplayOrientation)) { - if (maybeDoubleSpacePeriod()) { - mSpaceState = SPACE_STATE_DOUBLE; - } else if (!isShowingPunctuationList()) { - mSpaceState = SPACE_STATE_WEAK; - } - } - - mHandler.startDoubleSpacePeriodTimer(); - mHandler.postUpdateSuggestionStrip(); - } else { - if (swapWeakSpace) { - swapSwapperAndSpace(); - mSpaceState = SPACE_STATE_SWAP_PUNCTUATION; - } else if (SPACE_STATE_PHANTOM == spaceState - && currentSettings.isUsuallyFollowedBySpace(primaryCode)) { - // If we are in phantom space state, and the user presses a separator, we want to - // stay in phantom space state so that the next keypress has a chance to add the - // space. For example, if I type "Good dat", pick "day" from the suggestion strip - // then insert a comma and go on to typing the next word, I want the space to be - // inserted automatically before the next word, the same way it is when I don't - // input the comma. - // The case is a little different if the separator is a space stripper. Such a - // separator does not normally need a space on the right (that's the difference - // between swappers and strippers), so we should not stay in phantom space state if - // the separator is a stripper. Hence the additional test above. - mSpaceState = SPACE_STATE_PHANTOM; - } - - // Set punctuation right away. onUpdateSelection will fire but tests whether it is - // already displayed or not, so it's okay. - setPunctuationSuggestions(); - } - if (currentSettings.mIsInternal) { - LatinImeLoggerUtils.onSeparator((char)primaryCode, x, y); - } - - mKeyboardSwitcher.updateShiftState(); - return didAutoCorrect; - } - - private CharSequence getTextWithUnderline(final String text) { - return mIsAutoCorrectionIndicatorOn - ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(this, text) - : text; + public boolean hasSuggestionStripView() { + return null != mSuggestionStripView; } - private void handleClose() { - // TODO: Verify that words are logged properly when IME is closed. - commitTyped(LastComposedWord.NOT_A_SEPARATOR); - requestHideSelf(0); - final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); - if (mainKeyboardView != null) { - mainKeyboardView.closing(); - } - } - - // TODO: make this private - // Outside LatinIME, only used by the test suite. - @UsedForTesting - boolean isShowingPunctuationList() { - if (mSuggestedWords == null) return false; - return mSettings.getCurrent().mSuggestPuncList == mSuggestedWords; - } - - private boolean isSuggestionsStripVisible() { - final SettingsValues currentSettings = mSettings.getCurrent(); - if (mSuggestionStripView == null) - return false; - if (mSuggestionStripView.isShowingAddToDictionaryHint()) - return true; - if (null == currentSettings) - return false; - if (!currentSettings.isSuggestionStripVisibleInOrientation(mDisplayOrientation)) - return false; - if (currentSettings.isApplicationSpecifiedCompletionsOn()) - return true; - return currentSettings.isSuggestionsRequested(mDisplayOrientation); - } - - private void clearSuggestionStrip() { - setSuggestedWords(SuggestedWords.EMPTY, false); - setAutoCorrectionIndicator(false); + @Override + public boolean isShowingAddToDictionaryHint() { + return hasSuggestionStripView() && mSuggestionStripView.isShowingAddToDictionaryHint(); } - private void setSuggestedWords(final SuggestedWords words, final boolean isAutoCorrection) { - mSuggestedWords = words; - if (mSuggestionStripView != null) { - mSuggestionStripView.setSuggestions(words); - mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection); + @Override + public void dismissAddToDictionaryHint() { + if (!hasSuggestionStripView()) { + return; } + mSuggestionStripView.dismissAddToDictionaryHint(); } - private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) { - // Put a blue underline to a word in TextView which will be auto-corrected. - if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator - && mWordComposer.isComposingWord()) { - mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator; - final CharSequence textWithUnderline = - getTextWithUnderline(mWordComposer.getTypedWord()); - // TODO: when called from an updateSuggestionStrip() call that results from a posted - // message, this is called outside any batch edit. Potentially, this may result in some - // janky flickering of the screen, although the display speed makes it unlikely in - // the practice. - mConnection.setComposingText(textWithUnderline, 1); + private void setSuggestedWords(final SuggestedWords suggestedWords) { + mInputLogic.setSuggestedWords(suggestedWords); + // TODO: Modify this when we support suggestions with hard keyboard + if (!hasSuggestionStripView()) { + return; } - } - - private void updateSuggestionStrip() { - mHandler.cancelUpdateSuggestionStrip(); - final SettingsValues currentSettings = mSettings.getCurrent(); - - // Check if we have a suggestion engine attached. - if (mSuggest == null - || !currentSettings.isSuggestionsRequested(mDisplayOrientation)) { - if (mWordComposer.isComposingWord()) { - Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not " - + "requested!"); - } + if (!onEvaluateInputViewShown()) { return; } - if (!mWordComposer.isComposingWord() && !currentSettings.mBigramPredictionEnabled) { - setPunctuationSuggestions(); + final SettingsValues currentSettingsValues = mSettings.getCurrent(); + final boolean shouldShowImportantNotice = + ImportantNoticeUtils.shouldShowImportantNotice(this); + final boolean shouldShowSuggestionCandidates = + currentSettingsValues.mInputAttributes.mShouldShowSuggestions + && currentSettingsValues.isCurrentOrientationAllowingSuggestionsPerUserSettings(); + final boolean shouldShowSuggestionsStripUnlessPassword = shouldShowImportantNotice + || currentSettingsValues.mShowsVoiceInputKey + || shouldShowSuggestionCandidates + || currentSettingsValues.isApplicationSpecifiedCompletionsOn(); + final boolean shouldShowSuggestionsStrip = shouldShowSuggestionsStripUnlessPassword + && !currentSettingsValues.mInputAttributes.mIsPasswordField; + mSuggestionStripView.updateVisibility(shouldShowSuggestionsStrip, isFullscreenMode()); + if (!shouldShowSuggestionsStrip) { return; } - final AsyncResultHolder<SuggestedWords> holder = new AsyncResultHolder<SuggestedWords>(); - getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_TYPING, - SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() { - @Override - public void onGetSuggestedWords(final SuggestedWords suggestedWords) { - holder.set(suggestedWords); - } - } - ); + final boolean isEmptyApplicationSpecifiedCompletions = + currentSettingsValues.isApplicationSpecifiedCompletionsOn() + && suggestedWords.isEmpty(); + final boolean noSuggestionsToShow = (SuggestedWords.EMPTY == suggestedWords) + || suggestedWords.isPunctuationSuggestions() + || isEmptyApplicationSpecifiedCompletions; + if (shouldShowImportantNotice && noSuggestionsToShow) { + if (mSuggestionStripView.maybeShowImportantNoticeTitle()) { + return; + } + } - // This line may cause the current thread to wait. - final SuggestedWords suggestedWords = holder.get(null, GET_SUGGESTED_WORDS_TIMEOUT); - if (suggestedWords != null) { - showSuggestionStrip(suggestedWords); + if (currentSettingsValues.isCurrentOrientationAllowingSuggestionsPerUserSettings() + // We should clear suggestions if there is no suggestion to show. + || noSuggestionsToShow + || currentSettingsValues.isApplicationSpecifiedCompletionsOn()) { + mSuggestionStripView.setSuggestions(suggestedWords, + SubtypeLocaleUtils.isRtlLanguage(mSubtypeSwitcher.getCurrentSubtype())); } } - private void getSuggestedWords(final int sessionId, final int sequenceNumber, + // TODO[IL]: Move this out of LatinIME. + public void getSuggestedWords(final int sessionId, final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); - final Suggest suggest = mSuggest; - if (keyboard == null || suggest == null) { + if (keyboard == null) { callback.onGetSuggestedWords(SuggestedWords.EMPTY); return; } - // 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. - final SettingsValues currentSettings = mSettings.getCurrent(); - final int[] additionalFeaturesOptions = currentSettings.mAdditionalFeaturesSettingValues; - final String prevWord; - if (currentSettings.mCurrentLanguageHasSpaces) { - // If we are typing in a language with spaces we can just look up the previous - // word from textview. - prevWord = mConnection.getNthPreviousWord(currentSettings.mWordSeparators, - mWordComposer.isComposingWord() ? 2 : 1); - } else { - prevWord = LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? null - : mLastComposedWord.mCommittedWord; - } - suggest.getSuggestedWords(mWordComposer, prevWord, keyboard.getProximityInfo(), - currentSettings.mBlockPotentiallyOffensive, currentSettings.mCorrectionEnabled, - additionalFeaturesOptions, sessionId, sequenceNumber, callback); - } - - private void getSuggestedWordsOrOlderSuggestionsAsync(final int sessionId, - final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { - mInputUpdater.getSuggestedWords(sessionId, sequenceNumber, - new OnGetSuggestedWordsCallback() { - @Override - public void onGetSuggestedWords(SuggestedWords suggestedWords) { - callback.onGetSuggestedWords(maybeRetrieveOlderSuggestions( - mWordComposer.getTypedWord(), suggestedWords)); - } - }); - } - - private SuggestedWords maybeRetrieveOlderSuggestions(final String typedWord, - final SuggestedWords suggestedWords) { - // TODO: consolidate this into getSuggestedWords - // We update the suggestion strip only when we have some suggestions to show, i.e. when - // the suggestion count is > 1; else, we leave the old suggestions, with the typed word - // replaced with the new one. However, when the word is a dictionary word, or when the - // length of the typed word is 1 or 0 (after a deletion typically), we do want to remove the - // old suggestions. Also, if we are showing the "add to dictionary" hint, we need to - // revert to suggestions - although it is unclear how we can come here if it's displayed. - if (suggestedWords.size() > 1 || typedWord.length() <= 1 - || suggestedWords.mTypedWordValid || null == mSuggestionStripView - || mSuggestionStripView.isShowingAddToDictionaryHint()) { - return suggestedWords; - } else { - return getOlderSuggestions(typedWord); - } - } - - private SuggestedWords getOlderSuggestions(final String typedWord) { - SuggestedWords previousSuggestedWords = mSuggestedWords; - if (previousSuggestedWords == mSettings.getCurrent().mSuggestPuncList) { - previousSuggestedWords = SuggestedWords.EMPTY; - } - if (typedWord == null) { - return previousSuggestedWords; - } - final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions = - SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord, - previousSuggestedWords); - return new SuggestedWords(typedWordAndPreviousSuggestions, - false /* typedWordValid */, - false /* hasAutoCorrectionCandidate */, - false /* isPunctuationSuggestions */, - true /* isObsoleteSuggestions */, - false /* isPrediction */); + mInputLogic.getSuggestedWords(mSettings.getCurrent(), keyboard.getProximityInfo(), + mKeyboardSwitcher.getKeyboardShiftMode(), sessionId, sequenceNumber, callback); } - private void setAutoCorrection(final SuggestedWords suggestedWords, final String typedWord) { - if (suggestedWords.isEmpty()) return; - final String autoCorrection; - if (suggestedWords.mWillAutoCorrect) { - autoCorrection = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION); + @Override + public void showSuggestionStrip(final SuggestedWords sourceSuggestedWords) { + final SuggestedWords suggestedWords = + sourceSuggestedWords.isEmpty() ? SuggestedWords.EMPTY : sourceSuggestedWords; + if (SuggestedWords.EMPTY == suggestedWords) { + setNeutralSuggestionStrip(); } else { - // We can't use suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD) - // because it may differ from mWordComposer.mTypedWord. - autoCorrection = typedWord; - } - mWordComposer.setAutoCorrection(autoCorrection); - } - - private void showSuggestionStripWithTypedWord(final SuggestedWords suggestedWords, - final String typedWord) { - if (suggestedWords.isEmpty()) { - // No auto-correction is available, clear the cached values. - AccessibilityUtils.getInstance().setAutoCorrection(null, null); - clearSuggestionStrip(); - return; - } - setAutoCorrection(suggestedWords, typedWord); - final boolean isAutoCorrection = suggestedWords.willAutoCorrect(); - setSuggestedWords(suggestedWords, isAutoCorrection); - setAutoCorrectionIndicator(isAutoCorrection); - setSuggestionStripShown(isSuggestionsStripVisible()); - // An auto-correction is available, cache it in accessibility code so - // we can be speak it if the user touches a key that will insert it. - AccessibilityUtils.getInstance().setAutoCorrection(suggestedWords, typedWord); - } - - private void showSuggestionStrip(final SuggestedWords suggestedWords) { - if (suggestedWords.isEmpty()) { - clearSuggestionStrip(); - return; - } - showSuggestionStripWithTypedWord(suggestedWords, - suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD)); - } - - private void commitCurrentAutoCorrection(final String separator) { - // Complete any pending suggestions query first - if (mHandler.hasPendingUpdateSuggestions()) { - updateSuggestionStrip(); - } - final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull(); - final String typedWord = mWordComposer.getTypedWord(); - final String autoCorrection = (typedAutoCorrection != null) - ? typedAutoCorrection : typedWord; - if (autoCorrection != null) { - if (TextUtils.isEmpty(typedWord)) { - throw new RuntimeException("We have an auto-correction but the typed word " - + "is empty? Impossible! I must commit suicide."); - } - if (mSettings.isInternal()) { - LatinImeLoggerUtils.onAutoCorrection( - typedWord, autoCorrection, separator, mWordComposer); - } - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - final SuggestedWords suggestedWords = mSuggestedWords; - ResearchLogger.latinIme_commitCurrentAutoCorrection(typedWord, autoCorrection, - separator, mWordComposer.isBatchMode(), suggestedWords); - } - mExpectingUpdateSelection = true; - commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD, - separator); - if (!typedWord.equals(autoCorrection)) { - // This will make the correction flash for a short while as a visual clue - // to the user that auto-correction happened. It has no other effect; in particular - // note that this won't affect the text inside the text field AT ALL: it only makes - // the segment of text starting at the supplied index and running for the length - // of the auto-correction flash. At this moment, the "typedWord" argument is - // ignored by TextView. - mConnection.commitCorrection( - new CorrectionInfo(mLastSelectionEnd - typedWord.length(), - typedWord, autoCorrection)); - } + setSuggestedWords(suggestedWords); } + // Cache the auto-correction in accessibility code so we can speak it if the user + // touches a key that will insert it. + AccessibilityUtils.getInstance().setAutoCorrection(suggestedWords, + sourceSuggestedWords.mTypedWord); } // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener} // interface @Override - public void pickSuggestionManually(final int index, final SuggestedWordInfo suggestionInfo) { - final SuggestedWords suggestedWords = mSuggestedWords; - final String suggestion = suggestionInfo.mWord; - // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput - if (suggestion.length() == 1 && isShowingPunctuationList()) { - // Word separators are suggested before the user inputs something. - // So, LatinImeLogger logs "" as a user's input. - LatinImeLogger.logOnManualSuggestion("", suggestion, index, suggestedWords); - // Rely on onCodeInput to do the complicated swapping/stripping logic consistently. - final int primaryCode = suggestion.charAt(0); - onCodeInput(primaryCode, - Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_punctuationSuggestion(index, suggestion, - false /* isBatchMode */, suggestedWords.mIsPrediction); - } - return; - } - - mConnection.beginBatchEdit(); - final SettingsValues currentSettings = mSettings.getCurrent(); - if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0 - // In the batch input mode, a manually picked suggested word should just replace - // the current batch input text and there is no need for a phantom space. - && !mWordComposer.isBatchMode()) { - final int firstChar = Character.codePointAt(suggestion, 0); - if (!currentSettings.isWordSeparator(firstChar) - || currentSettings.isUsuallyPrecededBySpace(firstChar)) { - promotePhantomSpace(); - } - } - - if (currentSettings.isApplicationSpecifiedCompletionsOn() - && mApplicationSpecifiedCompletions != null - && index >= 0 && index < mApplicationSpecifiedCompletions.length) { - mSuggestedWords = SuggestedWords.EMPTY; - if (mSuggestionStripView != null) { - mSuggestionStripView.clear(); - } - mKeyboardSwitcher.updateShiftState(); - resetComposingState(true /* alsoResetLastComposedWord */); - final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index]; - mConnection.commitCompletion(completionInfo); - mConnection.endBatchEdit(); - return; - } - - // We need to log before we commit, because the word composer will store away the user - // typed word. - final String replacedWord = mWordComposer.getTypedWord(); - LatinImeLogger.logOnManualSuggestion(replacedWord, suggestion, index, suggestedWords); - mExpectingUpdateSelection = true; - commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK, - LastComposedWord.NOT_A_SEPARATOR); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion, - mWordComposer.isBatchMode(), suggestionInfo.mScore, suggestionInfo.mKind, - suggestionInfo.mSourceDict.mDictType); - } - mConnection.endBatchEdit(); - // Don't allow cancellation of manual pick - mLastComposedWord.deactivate(); - // Space state must be updated before calling updateShiftState - mSpaceState = SPACE_STATE_PHANTOM; - mKeyboardSwitcher.updateShiftState(); - - // We should show the "Touch again to save" hint if the user pressed the first entry - // AND it's in none of our current dictionaries (main, user or otherwise). - // Please note that if mSuggest is null, it means that everything is off: suggestion - // and correction, so we shouldn't try to show the hint - final Suggest suggest = mSuggest; - final boolean showingAddToDictionaryHint = - (SuggestedWordInfo.KIND_TYPED == suggestionInfo.mKind - || SuggestedWordInfo.KIND_OOV_CORRECTION == suggestionInfo.mKind) - && suggest != null - // If the suggestion is not in the dictionary, the hint should be shown. - && !AutoCorrectionUtils.isValidWord(suggest, suggestion, true); - - if (currentSettings.mIsInternal) { - LatinImeLoggerUtils.onSeparator((char)Constants.CODE_SPACE, - Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); - } - if (showingAddToDictionaryHint && mIsUserDictionaryAvailable) { - mSuggestionStripView.showAddToDictionaryHint( - suggestion, currentSettings.mHintToSaveText); - } else { - // If we're not showing the "Touch again to save", then update the suggestion strip. - mHandler.postUpdateSuggestionStrip(); - } - } - - /** - * Commits the chosen word to the text field and saves it for later retrieval. - */ - private void commitChosenWord(final String chosenWord, final int commitType, - final String separatorString) { - final SuggestedWords suggestedWords = mSuggestedWords; - mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan( - this, chosenWord, suggestedWords, mIsMainDictionaryAvailable), 1); - // Add the word to the user history dictionary - final String prevWord = addToUserHistoryDictionary(chosenWord); - // TODO: figure out here if this is an auto-correct or if the best word is actually - // what user typed. Note: currently this is done much later in - // LastComposedWord#didCommitTypedWord by string equality of the remembered - // strings. - mLastComposedWord = mWordComposer.commitWord(commitType, chosenWord, separatorString, - prevWord); - } - - private void setPunctuationSuggestions() { - final SettingsValues currentSettings = mSettings.getCurrent(); - if (currentSettings.mBigramPredictionEnabled) { - clearSuggestionStrip(); - } else { - setSuggestedWords(currentSettings.mSuggestPuncList, false); - } - setAutoCorrectionIndicator(false); - setSuggestionStripShown(isSuggestionsStripVisible()); - } - - private String addToUserHistoryDictionary(final String suggestion) { - if (TextUtils.isEmpty(suggestion)) return null; - final Suggest suggest = mSuggest; - if (suggest == null) return null; - - // If correction is not enabled, we don't add words to the user history dictionary. - // That's to avoid unintended additions in some sensitive fields, or fields that - // expect to receive non-words. - final SettingsValues currentSettings = mSettings.getCurrent(); - if (!currentSettings.mCorrectionEnabled) return null; - - final UserHistoryDictionary userHistoryDictionary = mUserHistoryDictionary; - if (userHistoryDictionary == null) return null; - - final String prevWord = mConnection.getNthPreviousWord(currentSettings.mWordSeparators, 2); - final String secondWord; - if (mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps()) { - secondWord = suggestion.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale()); - } else { - secondWord = suggestion; - } - // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid". - // We don't add words with 0-frequency (assuming they would be profanity etc.). - final int maxFreq = AutoCorrectionUtils.getMaxFrequency( - suggest.getUnigramDictionaries(), suggestion); - if (maxFreq == 0) return null; - userHistoryDictionary.addToDictionary(prevWord, secondWord, maxFreq > 0); - return prevWord; - } - - private boolean isResumableWord(final String word, final SettingsValues settings) { - final int firstCodePoint = word.codePointAt(0); - return settings.isWordCodePoint(firstCodePoint) - && Constants.CODE_SINGLE_QUOTE != firstCodePoint - && Constants.CODE_DASH != firstCodePoint; - } - - /** - * Check if the cursor is touching a word. If so, restart suggestions on this word, else - * do nothing. - */ - private void restartSuggestionsOnWordTouchedByCursor() { - // HACK: We may want to special-case some apps that exhibit bad behavior in case of - // recorrection. This is a temporary, stopgap measure that will be removed later. - // TODO: remove this. - if (mAppWorkAroundsUtils.isBrokenByRecorrection()) return; - // A simple way to test for support from the TextView. - if (!isSuggestionsStripVisible()) return; - // Recorrection is not supported in languages without spaces because we don't know - // how to segment them yet. - if (!mSettings.getCurrent().mCurrentLanguageHasSpaces) return; - // If the cursor is not touching a word, or if there is a selection, return right away. - if (mLastSelectionStart != mLastSelectionEnd) return; - // If we don't know the cursor location, return. - if (mLastSelectionStart < 0) return; - final SettingsValues currentSettings = mSettings.getCurrent(); - if (!mConnection.isCursorTouchingWord(currentSettings)) return; - final TextRange range = mConnection.getWordRangeAtCursor(currentSettings.mWordSeparators, - 0 /* additionalPrecedingWordsCount */); - if (null == range) return; // Happens if we don't have an input connection at all - if (range.length() <= 0) return; // Race condition. No text to resume on, so bail out. - // If for some strange reason (editor bug or so) we measure the text before the cursor as - // longer than what the entire text is supposed to be, the safe thing to do is bail out. - final int numberOfCharsInWordBeforeCursor = range.getNumberOfCharsInWordBeforeCursor(); - if (numberOfCharsInWordBeforeCursor > mLastSelectionStart) return; - final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList(); - final String typedWord = range.mWord.toString(); - if (!isResumableWord(typedWord, currentSettings)) return; - int i = 0; - for (final SuggestionSpan span : range.getSuggestionSpansAtWord()) { - for (final String s : span.getSuggestions()) { - ++i; - if (!TextUtils.equals(s, typedWord)) { - suggestions.add(new SuggestedWordInfo(s, - SuggestionStripView.MAX_SUGGESTIONS - i, - SuggestedWordInfo.KIND_RESUMED, Dictionary.DICTIONARY_RESUMED, - SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, - SuggestedWordInfo.NOT_A_CONFIDENCE - /* autoCommitFirstWordConfidence */)); - } - } - } - mWordComposer.setComposingWord(typedWord, mKeyboardSwitcher.getKeyboard()); - mWordComposer.setCursorPositionWithinWord( - typedWord.codePointCount(0, numberOfCharsInWordBeforeCursor)); - mConnection.setComposingRegion( - mLastSelectionStart - numberOfCharsInWordBeforeCursor, - mLastSelectionEnd + range.getNumberOfCharsInWordAfterCursor()); - if (suggestions.isEmpty()) { - // We come here if there weren't any suggestion spans on this word. We will try to - // compute suggestions for it instead. - mInputUpdater.getSuggestedWords(Suggest.SESSION_TYPING, - SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() { - @Override - public void onGetSuggestedWords( - final SuggestedWords suggestedWordsIncludingTypedWord) { - final SuggestedWords suggestedWords; - if (suggestedWordsIncludingTypedWord.size() > 1) { - // We were able to compute new suggestions for this word. - // Remove the typed word, since we don't want to display it in this - // case. The #getSuggestedWordsExcludingTypedWord() method sets - // willAutoCorrect to false. - suggestedWords = suggestedWordsIncludingTypedWord - .getSuggestedWordsExcludingTypedWord(); - } else { - // No saved suggestions, and we were unable to compute any good one - // either. Rather than displaying an empty suggestion strip, we'll - // display the original word alone in the middle. - // Since there is only one word, willAutoCorrect is false. - suggestedWords = suggestedWordsIncludingTypedWord; - } - // We need to pass typedWord because mWordComposer.mTypedWord may - // differ from typedWord. - unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip( - suggestedWords, typedWord); - }}); - } else { - // We found suggestion spans in the word. We'll create the SuggestedWords out of - // them, and make willAutoCorrect false. - final SuggestedWords suggestedWords = new SuggestedWords(suggestions, - true /* typedWordValid */, false /* willAutoCorrect */, - false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */, - false /* isPrediction */); - // We need to pass typedWord because mWordComposer.mTypedWord may differ from typedWord. - unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(suggestedWords, typedWord); - } - } - - public void unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip( - final SuggestedWords suggestedWords, final String typedWord) { - // Note that it's very important here that suggestedWords.mWillAutoCorrect is false. - // We never want to auto-correct on a resumed suggestion. Please refer to the three places - // above in restartSuggestionsOnWordTouchedByCursor() where suggestedWords is affected. - // We also need to unset mIsAutoCorrectionIndicatorOn to avoid showSuggestionStrip touching - // the text to adapt it. - // TODO: remove mIsAutoCorrectionIndicatorOn (see comment on definition) - mIsAutoCorrectionIndicatorOn = false; - mHandler.showSuggestionStripWithTypedWord(suggestedWords, typedWord); - } - - /** - * Check if the cursor is actually at the end of a word. If so, restart suggestions on this - * word, else do nothing. - */ - private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() { - final CharSequence word = - mConnection.getWordBeforeCursorIfAtEndOfWord(mSettings.getCurrent()); - if (null != word) { - final String wordString = word.toString(); - restartSuggestionsOnWordBeforeCursor(wordString); - // TODO: Handle the case where the user manually moves the cursor and then backs up over - // a separator. In that case, the current log unit should not be uncommitted. - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.getInstance().uncommitCurrentLogUnit(wordString, - true /* dumpCurrentLogUnit */); - } - } + public void pickSuggestionManually(final SuggestedWordInfo suggestionInfo) { + final InputTransaction completeInputTransaction = mInputLogic.onPickSuggestionManually( + mSettings.getCurrent(), suggestionInfo, + mKeyboardSwitcher.getKeyboardShiftMode(), + mKeyboardSwitcher.getCurrentKeyboardScriptId(), + mHandler); + updateStateAfterInputTransaction(completeInputTransaction); } - private void restartSuggestionsOnWordBeforeCursor(final String word) { - mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard()); - final int length = word.length(); - mConnection.deleteSurroundingText(length, 0); - mConnection.setComposingText(word, 1); - mHandler.postUpdateSuggestionStrip(); - } - - /** - * Retry resetting caches in the rich input connection. - * - * When the editor can't be accessed we can't reset the caches, so we schedule a retry. - * This method handles the retry, and re-schedules a new retry if we still can't access. - * We only retry up to 5 times before giving up. - * - * @param tryResumeSuggestions Whether we should resume suggestions or not. - * @param remainingTries How many times we may try again before giving up. - */ - private void retryResetCaches(final boolean tryResumeSuggestions, final int remainingTries) { - if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(mLastSelectionStart, false)) { - if (0 < remainingTries) { - mHandler.postResetCaches(tryResumeSuggestions, remainingTries - 1); - return; - } - // If remainingTries is 0, we should stop waiting for new tries, but it's still - // better to load the keyboard (less things will be broken). - } - tryFixLyingCursorPosition(); - mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent()); - if (tryResumeSuggestions) mHandler.postResumeSuggestions(); - } - - private void revertCommit() { - final String previousWord = mLastComposedWord.mPrevWord; - final String originallyTypedWord = mLastComposedWord.mTypedWord; - final String committedWord = mLastComposedWord.mCommittedWord; - final int cancelLength = committedWord.length(); - // We want java chars, not codepoints for the following. - final int separatorLength = mLastComposedWord.mSeparatorString.length(); - // TODO: should we check our saved separator against the actual contents of the text view? - final int deleteLength = cancelLength + separatorLength; - if (DEBUG) { - if (mWordComposer.isComposingWord()) { - throw new RuntimeException("revertCommit, but we are composing a word"); - } - final CharSequence wordBeforeCursor = - mConnection.getTextBeforeCursor(deleteLength, 0) - .subSequence(0, cancelLength); - if (!TextUtils.equals(committedWord, wordBeforeCursor)) { - throw new RuntimeException("revertCommit check failed: we thought we were " - + "reverting \"" + committedWord - + "\", but before the cursor we found \"" + wordBeforeCursor + "\""); - } - } - mConnection.deleteSurroundingText(deleteLength, 0); - if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) { - mUserHistoryDictionary.cancelAddingUserHistory(previousWord, committedWord); - } - final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString; - if (mSettings.getCurrent().mCurrentLanguageHasSpaces) { - // For languages with spaces, we revert to the typed string, but the cursor is still - // after the separator so we don't resume suggestions. If the user wants to correct - // the word, they have to press backspace again. - mConnection.commitText(stringToCommit, 1); - } else { - // For languages without spaces, we revert the typed string but the cursor is flush - // with the typed word, so we need to resume suggestions right away. - mWordComposer.setComposingWord(stringToCommit, mKeyboardSwitcher.getKeyboard()); - mConnection.setComposingText(stringToCommit, 1); - } - if (mSettings.isInternal()) { - LatinImeLoggerUtils.onSeparator(mLastComposedWord.mSeparatorString, - Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); - } - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_revertCommit(committedWord, originallyTypedWord, - mWordComposer.isBatchMode(), mLastComposedWord.mSeparatorString); + @Override + public void showAddToDictionaryHint(final String word) { + if (!hasSuggestionStripView()) { + return; } - // Don't restart suggestion yet. We'll restart if the user deletes the - // separator. - mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; - // We have a separator between the word and the cursor: we should show predictions. - mHandler.postUpdateSuggestionStrip(); + mSuggestionStripView.showAddToDictionaryHint(word); } - // This essentially inserts a space, and that's it. - public void promotePhantomSpace() { + // This will show either an empty suggestion strip (if prediction is enabled) or + // punctuation suggestions (if it's disabled). + @Override + public void setNeutralSuggestionStrip() { final SettingsValues currentSettings = mSettings.getCurrent(); - if (currentSettings.shouldInsertSpacesAutomatically() - && currentSettings.mCurrentLanguageHasSpaces - && !mConnection.textBeforeCursorLooksLikeURL()) { - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_promotePhantomSpace(); - } - sendKeyCodePoint(Constants.CODE_SPACE); - } + final SuggestedWords neutralSuggestions = currentSettings.mBigramPredictionEnabled + ? SuggestedWords.EMPTY : currentSettings.mSpacingAndPunctuations.mSuggestPuncList; + setSuggestedWords(neutralSuggestions); } // TODO: Make this private @@ -3089,18 +1476,44 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen loadSettings(); if (mKeyboardSwitcher.getMainKeyboardView() != null) { // Reload keyboard because the current language has been changed. - mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent()); + mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent(), + getCurrentAutoCapsState(), getCurrentRecapitalizeState()); + } + } + + /** + * After an input transaction has been executed, some state must be updated. This includes + * the shift state of the keyboard and suggestions. This method looks at the finished + * inputTransaction to find out what is necessary and updates the state accordingly. + * @param inputTransaction The transaction that has been executed. + */ + private void updateStateAfterInputTransaction(final InputTransaction inputTransaction) { + switch (inputTransaction.getRequiredShiftUpdate()) { + case InputTransaction.SHIFT_UPDATE_LATER: + mHandler.postUpdateShiftState(); + break; + case InputTransaction.SHIFT_UPDATE_NOW: + mKeyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(), + getCurrentRecapitalizeState()); + break; + default: // SHIFT_NO_UPDATE + } + if (inputTransaction.requiresUpdateSuggestions()) { + mHandler.postUpdateSuggestionStrip(); + } + if (inputTransaction.didAffectContents()) { + mSubtypeState.setCurrentSubtypeHasBeenUsed(); } } private void hapticAndAudioFeedback(final int code, final int repeatCount) { final MainKeyboardView keyboardView = mKeyboardSwitcher.getMainKeyboardView(); - if (keyboardView != null && keyboardView.isInSlidingKeyInput()) { - // No need to feedback while sliding input. + if (keyboardView != null && keyboardView.isInDraggingFinger()) { + // No need to feedback while finger is dragging. return; } if (repeatCount > 0) { - if (code == Constants.CODE_DELETE && !mConnection.canDeleteCharacters()) { + if (code == Constants.CODE_DELETE && !mInputLogic.mConnection.canDeleteCharacters()) { // No need to feedback when repeat delete key will have no effect. return; } @@ -3124,7 +1537,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void onPressKey(final int primaryCode, final int repeatCount, final boolean isSinglePointer) { - mKeyboardSwitcher.onPressKey(primaryCode, isSinglePointer); + mKeyboardSwitcher.onPressKey(primaryCode, isSinglePointer, getCurrentAutoCapsState(), + getCurrentRecapitalizeState()); hapticAndAudioFeedback(primaryCode, repeatCount); } @@ -3132,40 +1546,44 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // press matching call is {@link #onPressKey(int,int,boolean)} above. @Override public void onReleaseKey(final int primaryCode, final boolean withSliding) { - mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding); + mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding, getCurrentAutoCapsState(), + getCurrentRecapitalizeState()); + } - // If accessibility is on, ensure the user receives keyboard state updates. - if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { - switch (primaryCode) { - case Constants.CODE_SHIFT: - AccessibleKeyboardViewProxy.getInstance().notifyShiftState(); - break; - case Constants.CODE_SWITCH_ALPHA_SYMBOL: - AccessibleKeyboardViewProxy.getInstance().notifySymbolsState(); - break; - } - } + private HardwareEventDecoder getHardwareKeyEventDecoder(final int deviceId) { + final HardwareEventDecoder decoder = mHardwareEventDecoders.get(deviceId); + if (null != decoder) return decoder; + // TODO: create the decoder according to the specification + final HardwareEventDecoder newDecoder = new HardwareKeyboardEventDecoder(deviceId); + mHardwareEventDecoders.put(deviceId, newDecoder); + return newDecoder; } // Hooks for hardware keyboard @Override - public boolean onKeyDown(final int keyCode, final KeyEvent event) { - if (!ProductionFlag.IS_HARDWARE_KEYBOARD_SUPPORTED) return super.onKeyDown(keyCode, event); - // onHardwareKeyEvent, like onKeyDown returns true if it handled the event, false if - // it doesn't know what to do with it and leave it to the application. For example, - // hardware key events for adjusting the screen's brightness are passed as is. - if (mEventInterpreter.onHardwareKeyEvent(event)) { - final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode(); - mCurrentlyPressedHardwareKeys.add(keyIdentifier); + public boolean onKeyDown(final int keyCode, final KeyEvent keyEvent) { + if (!ProductionFlag.IS_HARDWARE_KEYBOARD_SUPPORTED) { + return super.onKeyDown(keyCode, keyEvent); + } + final Event event = getHardwareKeyEventDecoder( + keyEvent.getDeviceId()).decodeHardwareKey(keyEvent); + // If the event is not handled by LatinIME, we just pass it to the parent implementation. + // If it's handled, we return true because we did handle it. + if (event.isHandled()) { + mInputLogic.onCodeInput(mSettings.getCurrent(), event, + mKeyboardSwitcher.getKeyboardShiftMode(), + // TODO: this is not necessarily correct for a hardware keyboard right now + mKeyboardSwitcher.getCurrentKeyboardScriptId(), + mHandler); return true; } - return super.onKeyDown(keyCode, event); + return super.onKeyDown(keyCode, keyEvent); } @Override public boolean onKeyUp(final int keyCode, final KeyEvent event) { final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode(); - if (mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) { + if (mInputLogic.mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) { return true; } return super.onKeyUp(keyCode, event); @@ -3177,7 +1595,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // boolean onKeyMultiple(final int keyCode, final int count, final KeyEvent event); // receive ringer mode change and network state change. - private BroadcastReceiver mReceiver = new BroadcastReceiver() { + private final BroadcastReceiver mConnectivityAndRingerModeChangeReceiver = + new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) { final String action = intent.getAction(); @@ -3190,17 +1609,15 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen }; private void launchSettings() { - handleClose(); + mInputLogic.commitTyped(mSettings.getCurrent(), LastComposedWord.NOT_A_SEPARATOR); + requestHideSelf(0); + final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); + if (mainKeyboardView != null) { + mainKeyboardView.closing(); + } launchSubActivity(SettingsActivity.class); } - public void launchKeyboardedDialogActivity(final Class<? extends Activity> activityClass) { - // Put the text in the attached EditText into a safe, saved state before switching to a - // new activity that will also use the soft keyboard. - commitTyped(LastComposedWord.NOT_A_SEPARATOR); - launchSubActivity(activityClass); - } - private void launchSubActivity(final Class<? extends Activity> activityClass) { Intent intent = new Intent(); intent.setClass(LatinIME.this, activityClass); @@ -3215,9 +1632,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final CharSequence[] items = new CharSequence[] { // TODO: Should use new string "Select active input modes". getString(R.string.language_selection_title), - getString(ApplicationUtils.getAcitivityTitleResId(this, SettingsActivity.class)), + getString(ApplicationUtils.getActivityTitleResId(this, SettingsActivity.class)), }; - final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { + final OnClickListener listener = new OnClickListener() { @Override public void onClick(DialogInterface di, int position) { di.dismiss(); @@ -3226,8 +1643,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final Intent intent = IntentUtils.getInputLanguageSelectionIntent( mRichImm.getInputMethodIdOfThisIme(), Intent.FLAG_ACTIVITY_NEW_TASK - | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED - | Intent.FLAG_ACTIVITY_CLEAR_TOP); + | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + | Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); break; case 1: @@ -3236,21 +1653,22 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } }; - final AlertDialog.Builder builder = new AlertDialog.Builder(this) - .setItems(items, listener) - .setTitle(title); - showOptionDialog(builder.create()); + final AlertDialog.Builder builder = new AlertDialog.Builder( + DialogUtils.getPlatformDialogThemeContext(this)); + builder.setItems(items, listener).setTitle(title); + final AlertDialog dialog = builder.create(); + dialog.setCancelable(true /* cancelable */); + dialog.setCanceledOnTouchOutside(true /* cancelable */); + showOptionDialog(dialog); } - public void showOptionDialog(final AlertDialog dialog) { + // TODO: Move this method out of {@link LatinIME}. + private void showOptionDialog(final AlertDialog dialog) { final IBinder windowToken = mKeyboardSwitcher.getMainKeyboardView().getWindowToken(); if (windowToken == null) { return; } - dialog.setCancelable(true); - dialog.setCanceledOnTouchOutside(true); - final Window window = dialog.getWindow(); final WindowManager.LayoutParams lp = window.getAttributes(); lp.token = windowToken; @@ -3264,31 +1682,51 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // TODO: can this be removed somehow without breaking the tests? @UsedForTesting - /* package for test */ String getFirstSuggestedWord() { - return mSuggestedWords.size() > 0 ? mSuggestedWords.getWord(0) : null; + /* package for test */ SuggestedWords getSuggestedWordsForTest() { + // You may not use this method for anything else than debug + return DEBUG ? mInputLogic.mSuggestedWords : null; } // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME. @UsedForTesting - /* package for test */ boolean isCurrentlyWaitingForMainDictionary() { - return mSuggest.isCurrentlyWaitingForMainDictionary(); + /* package for test */ void waitForLoadingDictionaries(final long timeout, final TimeUnit unit) + throws InterruptedException { + mDictionaryFacilitator.waitForLoadingDictionariesForTesting(timeout, unit); } - // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME. + // DO NOT USE THIS for any other purpose than testing. This can break the keyboard badly. @UsedForTesting - /* package for test */ boolean hasMainDictionary() { - return mSuggest.hasMainDictionary(); + /* package for test */ void replaceDictionariesForTest(final Locale locale) { + final SettingsValues settingsValues = mSettings.getCurrent(); + mDictionaryFacilitator.resetDictionaries(this, locale, + settingsValues.mUseContactsDict, settingsValues.mUsePersonalizedDicts, + false /* forceReloadMainDictionary */, this /* listener */); + } + + // DO NOT USE THIS for any other purpose than testing. + @UsedForTesting + /* package for test */ void clearPersonalizedDictionariesForTest() { + mDictionaryFacilitator.clearUserHistoryDictionary(); + mDictionaryFacilitator.clearPersonalizationDictionary(); } - // DO NOT USE THIS for any other purpose than testing. This can break the keyboard badly. @UsedForTesting - /* package for test */ void replaceMainDictionaryForTest(final Locale locale) { - mSuggest.resetMainDict(this, locale, null); + /* package for test */ List<InputMethodSubtype> getEnabledSubtypesForTest() { + return (mRichImm != null) ? mRichImm.getMyEnabledInputMethodSubtypeList( + true /* allowsImplicitlySelectedSubtypes */) : new ArrayList<InputMethodSubtype>(); + } + + public void dumpDictionaryForDebug(final String dictName) { + if (mDictionaryFacilitator.getLocale() == null) { + resetSuggest(); + } + mDictionaryFacilitator.dumpDictionaryForDebug(dictName); } public void debugDumpStateAndCrashWithException(final String context) { - final StringBuilder s = new StringBuilder(mAppWorkAroundsUtils.toString()); - s.append("\nAttributes : ").append(mSettings.getCurrent().mInputAttributes) + final SettingsValues settingsValues = mSettings.getCurrent(); + final StringBuilder s = new StringBuilder(settingsValues.toString()); + s.append("\nAttributes : ").append(settingsValues.mInputAttributes) .append("\nContext : ").append(context); throw new RuntimeException(s.toString()); } @@ -3299,17 +1737,37 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final Printer p = new PrintWriterPrinter(fout); p.println("LatinIME state :"); + p.println(" VersionCode = " + ApplicationUtils.getVersionCode(this)); + p.println(" VersionName = " + ApplicationUtils.getVersionName(this)); final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1; p.println(" Keyboard mode = " + keyboardMode); final SettingsValues settingsValues = mSettings.getCurrent(); - p.println(" mIsSuggestionsSuggestionsRequested = " - + settingsValues.isSuggestionsRequested(mDisplayOrientation)); - p.println(" mCorrectionEnabled=" + settingsValues.mCorrectionEnabled); - p.println(" isComposingWord=" + mWordComposer.isComposingWord()); - p.println(" mSoundOn=" + settingsValues.mSoundOn); - p.println(" mVibrateOn=" + settingsValues.mVibrateOn); - p.println(" mKeyPreviewPopupOn=" + settingsValues.mKeyPreviewPopupOn); - p.println(" inputAttributes=" + settingsValues.mInputAttributes); + p.println(settingsValues.dump()); + // TODO: Dump all settings values + } + + public boolean shouldSwitchToOtherInputMethods() { + // TODO: Revisit here to reorganize the settings. Probably we can/should use different + // strategy once the implementation of + // {@link InputMethodManager#shouldOfferSwitchingToNextInputMethod} is defined well. + final boolean fallbackValue = mSettings.getCurrent().mIncludesOtherImesInLanguageSwitchList; + final IBinder token = getWindow().getWindow().getAttributes().token; + if (token == null) { + return fallbackValue; + } + return mRichImm.shouldOfferSwitchingToNextInputMethod(token, fallbackValue); + } + + public boolean shouldShowLanguageSwitchKey() { + // TODO: Revisit here to reorganize the settings. Probably we can/should use different + // strategy once the implementation of + // {@link InputMethodManager#shouldOfferSwitchingToNextInputMethod} is defined well. + final boolean fallbackValue = mSettings.getCurrent().isLanguageSwitchKeyEnabled(); + final IBinder token = getWindow().getWindow().getAttributes().token; + if (token == null) { + return fallbackValue; + } + return mRichImm.shouldOfferSwitchingToNextInputMethod(token, fallbackValue); } } diff --git a/java/src/com/android/inputmethod/latin/LatinImeLogger.java b/java/src/com/android/inputmethod/latin/LatinImeLogger.java index 3f2b0a3f4..8fd36b937 100644 --- a/java/src/com/android/inputmethod/latin/LatinImeLogger.java +++ b/java/src/com/android/inputmethod/latin/LatinImeLogger.java @@ -16,76 +16,12 @@ package com.android.inputmethod.latin; -import android.content.SharedPreferences; -import android.view.inputmethod.EditorInfo; +import android.content.Context; -import com.android.inputmethod.keyboard.Keyboard; +// TODO: Rename this class name to make it more relevant. +public final class LatinImeLogger { + public static final boolean sDBG = false; -public final class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChangeListener { - - public static boolean sDBG = false; - public static boolean sVISUALDEBUG = false; - public static boolean sUsabilityStudy = false; - - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - } - - public static void init(LatinIME context) { - } - - public static void commit() { - } - - public static boolean getUsabilityStudyMode(final SharedPreferences prefs) { - return false; - } - - public static void onDestroy() { - } - - public static void logOnManualSuggestion( - String before, String after, int position, SuggestedWords suggestedWords) { - } - - 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() { - } - - public static void logOnDelete(int x, int y) { - } - - public static void logOnInputChar() { - } - - public static void logOnInputSeparator() { - } - - public static void logOnException(String metaData, Throwable e) { - } - - public static void logOnWarning(String warning) { - } - - public static void onStartInputView(EditorInfo editorInfo) { - } - - public static void onStartSuggestion(CharSequence previousWords) { - } - - public static void onAddSuggestedWord(String word, String sourceDictionaryId) { - } - - public static void onSetKeyboard(Keyboard kb) { - } - - public static void onPrintAllUsabilityStudyLogs() { + public static void init(Context context) { } } diff --git a/java/src/com/android/inputmethod/latin/PrevWordsInfo.java b/java/src/com/android/inputmethod/latin/PrevWordsInfo.java new file mode 100644 index 000000000..f45c73f53 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/PrevWordsInfo.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin; + +import java.util.Arrays; + +import com.android.inputmethod.latin.utils.StringUtils; + +/** + * Class to represent information of previous words. This class is used to add n-gram entries + * into binary dictionaries, to get predictions, and to get suggestions. + */ +public class PrevWordsInfo { + public static final PrevWordsInfo EMPTY_PREV_WORDS_INFO = + new PrevWordsInfo(WordInfo.EMPTY_WORD_INFO); + public static final PrevWordsInfo BEGINNING_OF_SENTENCE = + new PrevWordsInfo(WordInfo.BEGINNING_OF_SENTENCE); + + /** + * Word information used to represent previous words information. + */ + public static class WordInfo { + public static final WordInfo EMPTY_WORD_INFO = new WordInfo(null); + public static final WordInfo BEGINNING_OF_SENTENCE = new WordInfo(); + + // This is an empty string when mIsBeginningOfSentence is true. + public final String mWord; + // TODO: Have sentence separator. + // Whether the current context is beginning of sentence or not. This is true when composing + // at the beginning of an input field or composing a word after a sentence separator. + public final boolean mIsBeginningOfSentence; + + // Beginning of sentence. + public WordInfo() { + mWord = ""; + mIsBeginningOfSentence = true; + } + + public WordInfo(final String word) { + mWord = word; + mIsBeginningOfSentence = false; + } + + public boolean isValid() { + return mWord != null; + } + + @Override + public int hashCode() { + return Arrays.hashCode(new Object[] { mWord, mIsBeginningOfSentence } ); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof WordInfo)) return false; + final WordInfo wordInfo = (WordInfo)o; + if (mWord == null || wordInfo.mWord == null) { + return mWord == wordInfo.mWord + && mIsBeginningOfSentence == wordInfo.mIsBeginningOfSentence; + } + return mWord.equals(wordInfo.mWord) + && mIsBeginningOfSentence == wordInfo.mIsBeginningOfSentence; + } + } + + // The words immediately before the considered word. EMPTY_WORD_INFO element means we don't + // have any context for that previous word including the "beginning of sentence context" - we + // just don't know what to predict using the information. An example of that is after a comma. + // For simplicity of implementation, elements may also be EMPTY_WORD_INFO transiently after the + // WordComposer was reset and before starting a new composing word, but we should never be + // calling getSuggetions* in this situation. + public WordInfo[] mPrevWordsInfo = new WordInfo[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM]; + + // Construct from the previous word information. + public PrevWordsInfo(final WordInfo prevWordInfo) { + mPrevWordsInfo[0] = prevWordInfo; + } + + // Construct from WordInfo array. n-th element represents (n+1)-th previous word's information. + public PrevWordsInfo(final WordInfo[] prevWordsInfo) { + for (int i = 0; i < Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM; i++) { + mPrevWordsInfo[i] = + (prevWordsInfo.length > i) ? prevWordsInfo[i] : WordInfo.EMPTY_WORD_INFO; + } + } + + // Create next prevWordsInfo using current prevWordsInfo. + public PrevWordsInfo getNextPrevWordsInfo(final WordInfo wordInfo) { + final WordInfo[] prevWordsInfo = new WordInfo[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM]; + prevWordsInfo[0] = wordInfo; + for (int i = 1; i < prevWordsInfo.length; i++) { + prevWordsInfo[i] = mPrevWordsInfo[i - 1]; + } + return new PrevWordsInfo(prevWordsInfo); + } + + public boolean isValid() { + return mPrevWordsInfo[0].isValid(); + } + + public void outputToArray(final int[][] codePointArrays, + final boolean[] isBeginningOfSentenceArray) { + for (int i = 0; i < mPrevWordsInfo.length; i++) { + final WordInfo wordInfo = mPrevWordsInfo[i]; + if (wordInfo == null || !wordInfo.isValid()) { + codePointArrays[i] = new int[0]; + isBeginningOfSentenceArray[i] = false; + continue; + } + codePointArrays[i] = StringUtils.toCodePointArray(wordInfo.mWord); + isBeginningOfSentenceArray[i] = wordInfo.mIsBeginningOfSentence; + } + } + + @Override + public int hashCode() { + return Arrays.hashCode(mPrevWordsInfo); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PrevWordsInfo)) return false; + final PrevWordsInfo prevWordsInfo = (PrevWordsInfo)o; + return Arrays.equals(mPrevWordsInfo, prevWordsInfo.mPrevWordsInfo); + } + + @Override + public String toString() { + final StringBuffer builder = new StringBuffer(); + for (int i = 0; i < mPrevWordsInfo.length; i++) { + final WordInfo wordInfo = mPrevWordsInfo[i]; + builder.append("PrevWord["); + builder.append(i); + builder.append("]: "); + if (wordInfo == null || !wordInfo.isValid()) { + builder.append("Empty. "); + continue; + } + builder.append(wordInfo.mWord); + builder.append(", isBeginningOfSentence: "); + builder.append(wordInfo.mIsBeginningOfSentence); + builder.append(". "); + } + return builder.toString(); + } +} diff --git a/java/src/com/android/inputmethod/latin/PunctuationSuggestions.java b/java/src/com/android/inputmethod/latin/PunctuationSuggestions.java new file mode 100644 index 000000000..0fba37c8a --- /dev/null +++ b/java/src/com/android/inputmethod/latin/PunctuationSuggestions.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin; + +import com.android.inputmethod.keyboard.internal.KeySpecParser; +import com.android.inputmethod.latin.utils.StringUtils; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * The extended {@link SuggestedWords} class to represent punctuation suggestions. + * + * Each punctuation specification string is the key specification that can be parsed by + * {@link KeySpecParser}. + */ +public final class PunctuationSuggestions extends SuggestedWords { + private PunctuationSuggestions(final ArrayList<SuggestedWordInfo> punctuationsList) { + super(punctuationsList, + null /* rawSuggestions */, + false /* typedWordValid */, + false /* hasAutoCorrectionCandidate */, + false /* isObsoleteSuggestions */, + false /* isPrediction */); + } + + /** + * Create new instance of {@link PunctuationSuggestions} from the array of punctuation key + * specifications. + * + * @param punctuationSpecs The array of punctuation key specifications. + * @return The {@link PunctuationSuggestions} object. + */ + public static PunctuationSuggestions newPunctuationSuggestions( + final String[] punctuationSpecs) { + final ArrayList<SuggestedWordInfo> puncuationsList = new ArrayList<>(); + for (final String puncSpec : punctuationSpecs) { + puncuationsList.add(newHardCodedWordInfo(puncSpec)); + } + return new PunctuationSuggestions(puncuationsList); + } + + /** + * {@inheritDoc} + * Note that {@link super#getWord(int)} returns a punctuation key specification text. + * The suggested punctuation should be gotten by parsing the key specification. + */ + @Override + public String getWord(final int index) { + final String keySpec = super.getWord(index); + final int code = KeySpecParser.getCode(keySpec); + return (code == Constants.CODE_OUTPUT_TEXT) + ? KeySpecParser.getOutputText(keySpec) + : StringUtils.newSingleCodePointString(code); + } + + /** + * {@inheritDoc} + * Note that {@link super#getWord(int)} returns a punctuation key specification text. + * The displayed text should be gotten by parsing the key specification. + */ + @Override + public String getLabel(final int index) { + final String keySpec = super.getWord(index); + return KeySpecParser.getLabel(keySpec); + } + + /** + * {@inheritDoc} + * Note that {@link #getWord(int)} returns a suggested punctuation. We should create a + * {@link SuggestedWordInfo} object that represents a hard coded word. + */ + @Override + public SuggestedWordInfo getInfo(final int index) { + return newHardCodedWordInfo(getWord(index)); + } + + /** + * The predicator to tell whether this object represents punctuation suggestions. + * @return true if this object represents punctuation suggestions. + */ + @Override + public boolean isPunctuationSuggestions() { + return true; + } + + @Override + public String toString() { + return "PunctuationSuggestions: " + + " words=" + Arrays.toString(mSuggestedWordInfoList.toArray()); + } + + private static SuggestedWordInfo newHardCodedWordInfo(final String keySpec) { + return new SuggestedWordInfo(keySpec, SuggestedWordInfo.MAX_SCORE, + SuggestedWordInfo.KIND_HARDCODED, + Dictionary.DICTIONARY_HARDCODED, + SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, + SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */); + } +} diff --git a/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java index 68505ce38..e59ef7563 100644 --- a/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java @@ -50,21 +50,14 @@ public final class ReadOnlyBinaryDictionary extends Dictionary { @Override public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, - final String prevWord, final ProximityInfo proximityInfo, - final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) { - return getSuggestionsWithSessionId(composer, prevWord, proximityInfo, blockOffensiveWords, - additionalFeaturesOptions, 0 /* sessionId */); - } - - @Override - public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer, - final String prevWord, final ProximityInfo proximityInfo, + final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, - final int sessionId) { + final int sessionId, final float[] inOutLanguageWeight) { if (mLock.readLock().tryLock()) { try { - return mBinaryDictionary.getSuggestions(composer, prevWord, proximityInfo, - blockOffensiveWords, additionalFeaturesOptions); + return mBinaryDictionary.getSuggestions(composer, prevWordsInfo, proximityInfo, + blockOffensiveWords, additionalFeaturesOptions, sessionId, + inOutLanguageWeight); } finally { mLock.readLock().unlock(); } @@ -73,10 +66,10 @@ public final class ReadOnlyBinaryDictionary extends Dictionary { } @Override - public boolean isValidWord(final String word) { + public boolean isInDictionary(final String word) { if (mLock.readLock().tryLock()) { try { - return mBinaryDictionary.isValidWord(word); + return mBinaryDictionary.isInDictionary(word); } finally { mLock.readLock().unlock(); } @@ -109,6 +102,18 @@ public final class ReadOnlyBinaryDictionary extends Dictionary { } @Override + public int getMaxFrequencyOfExactMatches(final String word) { + if (mLock.readLock().tryLock()) { + try { + return mBinaryDictionary.getMaxFrequencyOfExactMatches(word); + } finally { + mLock.readLock().unlock(); + } + } + return NOT_A_PROBABILITY; + } + + @Override public void close() { mLock.writeLock().lock(); try { diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java index 673d1b4c2..a6b3b710b 100644 --- a/java/src/com/android/inputmethod/latin/RichInputConnection.java +++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java @@ -26,17 +26,16 @@ import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; -import com.android.inputmethod.latin.define.ProductionFlag; -import com.android.inputmethod.latin.settings.SettingsValues; +import com.android.inputmethod.latin.settings.SpacingAndPunctuations; import com.android.inputmethod.latin.utils.CapsModeUtils; import com.android.inputmethod.latin.utils.DebugLogUtils; +import com.android.inputmethod.latin.utils.PrevWordsInfoUtils; +import com.android.inputmethod.latin.utils.ScriptUtils; import com.android.inputmethod.latin.utils.SpannableStringUtils; import com.android.inputmethod.latin.utils.StringUtils; import com.android.inputmethod.latin.utils.TextRange; -import com.android.inputmethod.research.ResearchLogger; -import java.util.Locale; -import java.util.regex.Pattern; +import java.util.Arrays; /** * Enrichment class for InputConnection to simplify interaction and add functionality. @@ -51,20 +50,26 @@ public final class RichInputConnection { 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 = Constants.DICTIONARY_MAX_WORD_LENGTH * 2 + 1; - private static final Pattern spaceRegex = Pattern.compile("\\s+"); + // Provision for long words and separators between the words. + private static final int LOOKBACK_CHARACTER_NUM = Constants.DICTIONARY_MAX_WORD_LENGTH + * (Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM + 1) /* words */ + + Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM /* separators */; private static final int INVALID_CURSOR_POSITION = -1; /** - * This variable contains an expected value for the cursor position. This is where the - * cursor may end up after all the keyboard-triggered updates have passed. We keep this to - * compare it to the actual cursor position to guess whether the move was caused by a - * keyboard command or not. - * It's not really the cursor position: the cursor may not be there yet, and it's also expected - * there be cases where it never actually comes to be there. + * This variable contains an expected value for the selection start position. This is where the + * cursor or selection start may end up after all the keyboard-triggered updates have passed. We + * keep this to compare it to the actual selection start to guess whether the move was caused by + * a keyboard command or not. + * It's not really the selection start position: the selection start may not be there yet, and + * in some cases, it may never arrive there. */ - private int mExpectedCursorPosition = INVALID_CURSOR_POSITION; // in chars, not code points + private int mExpectedSelStart = INVALID_CURSOR_POSITION; // in chars, not code points + /** + * The expected selection end. Only differs from mExpectedSelStart if a non-empty selection is + * expected. The same caveats as mExpectedSelStart apply. + */ + private int mExpectedSelEnd = INVALID_CURSOR_POSITION; // in chars, not code points /** * This contains the committed text immediately preceding the cursor and the composing * text if any. It is refreshed when the cursor moves by calling upon the TextView. @@ -93,7 +98,7 @@ public final class RichInputConnection { final ExtractedText et = mIC.getExtractedText(r, 0); final CharSequence beforeCursor = getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0); - final StringBuilder internal = new StringBuilder().append(mCommittedTextBeforeComposingText) + final StringBuilder internal = new StringBuilder(mCommittedTextBeforeComposingText) .append(mComposingText); if (null == et || null == beforeCursor) return; final int actualLength = Math.min(beforeCursor.length(), internal.length()); @@ -103,16 +108,16 @@ public final class RichInputConnection { final String reference = (beforeCursor.length() <= actualLength) ? beforeCursor.toString() : beforeCursor.subSequence(beforeCursor.length() - actualLength, beforeCursor.length()).toString(); - if (et.selectionStart != mExpectedCursorPosition + if (et.selectionStart != mExpectedSelStart || !(reference.equals(internal.toString()))) { - final String context = "Expected cursor position = " + mExpectedCursorPosition - + "\nActual cursor position = " + et.selectionStart + final String context = "Expected selection start = " + mExpectedSelStart + + "\nActual selection start = " + et.selectionStart + "\nExpected text = " + internal.length() + " " + internal + "\nActual text = " + reference.length() + " " + reference; ((LatinIME)mParent).debugDumpStateAndCrashWithException(context); } else { Log.e(TAG, DebugLogUtils.getStackTrace(2)); - Log.e(TAG, "Exp <> Actual : " + mExpectedCursorPosition + " <> " + et.selectionStart); + Log.e(TAG, "Exp <> Actual : " + mExpectedSelStart + " <> " + et.selectionStart); } } @@ -150,16 +155,35 @@ public final class RichInputConnection { * data, so we empty the cache and note that we don't know the new cursor position, and we * return false so that the caller knows about this and can retry later. * - * @param newCursorPosition The new position of the cursor, as received from the system. - * @param shouldFinishComposition Whether we should finish the composition in progress. + * @param newSelStart the new position of the selection start, as received from the system. + * @param newSelEnd the new position of the selection end, as received from the system. + * @param shouldFinishComposition whether we should finish the composition in progress. * @return true if we were able to connect to the editor successfully, false otherwise. When * this method returns false, the caches could not be correctly refreshed so they were only * reset: the caller should try again later to return to normal operation. */ - public boolean resetCachesUponCursorMoveAndReturnSuccess(final int newCursorPosition, - final boolean shouldFinishComposition) { - mExpectedCursorPosition = newCursorPosition; + public boolean resetCachesUponCursorMoveAndReturnSuccess(final int newSelStart, + final int newSelEnd, final boolean shouldFinishComposition) { + mExpectedSelStart = newSelStart; + mExpectedSelEnd = newSelEnd; mComposingText.setLength(0); + final boolean didReloadTextSuccessfully = reloadTextCache(); + if (!didReloadTextSuccessfully) { + Log.d(TAG, "Will try to retrieve text later."); + return false; + } + if (null != mIC && shouldFinishComposition) { + mIC.finishComposingText(); + } + return true; + } + + /** + * Reload the cached text from the InputConnection. + * + * @return true if successful + */ + private boolean reloadTextCache() { mCommittedTextBeforeComposingText.setLength(0); mIC = mParent.getCurrentInputConnection(); // Call upon the inputconnection directly since our own method is using the cache, and @@ -169,27 +193,12 @@ public final class RichInputConnection { if (null == textBeforeCursor) { // For some reason the app thinks we are not connected to it. This looks like a // framework bug... Fall back to ground state and return false. - mExpectedCursorPosition = INVALID_CURSOR_POSITION; - Log.e(TAG, "Unable to connect to the editor to retrieve text... will retry later"); + mExpectedSelStart = INVALID_CURSOR_POSITION; + mExpectedSelEnd = INVALID_CURSOR_POSITION; + Log.e(TAG, "Unable to connect to the editor to retrieve text."); return false; } mCommittedTextBeforeComposingText.append(textBeforeCursor); - final int lengthOfTextBeforeCursor = textBeforeCursor.length(); - if (lengthOfTextBeforeCursor > newCursorPosition - || (lengthOfTextBeforeCursor < Constants.EDITOR_CONTENTS_CACHE_SIZE - && newCursorPosition < Constants.EDITOR_CONTENTS_CACHE_SIZE)) { - // newCursorPosition may be lying -- when rotating the device (probably a framework - // bug). If we have less chars than we asked for, then we know how many chars we have, - // and if we got more than newCursorPosition says, then we know it was lying. In both - // cases the length is more reliable - mExpectedCursorPosition = lengthOfTextBeforeCursor; - } - if (null != mIC && shouldFinishComposition) { - mIC.finishComposingText(); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.richInputConnection_finishComposingText(); - } - } return true; } @@ -204,13 +213,13 @@ public final class RichInputConnection { public void finishComposingText() { if (DEBUG_BATCH_NESTING) checkBatchEdit(); if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); + // TODO: this is not correct! The cursor is not necessarily after the composing text. + // In the practice right now this is only called when input ends so it will be reset so + // it works, but it's wrong and should be fixed. mCommittedTextBeforeComposingText.append(mComposingText); mComposingText.setLength(0); if (null != mIC) { mIC.finishComposingText(); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.richInputConnection_finishComposingText(); - } } } @@ -218,7 +227,11 @@ public final class RichInputConnection { if (DEBUG_BATCH_NESTING) checkBatchEdit(); if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); mCommittedTextBeforeComposingText.append(text); - mExpectedCursorPosition += text.length() - mComposingText.length(); + // TODO: the following is exceedingly error-prone. Right now when the cursor is in the + // middle of the composing word mComposingText only holds the part of the composing text + // that is before the cursor, so this actually works, but it's terribly confusing. Fix this. + mExpectedSelStart += text.length() - mComposingText.length(); + mExpectedSelEnd = mExpectedSelStart; mComposingText.setLength(0); if (null != mIC) { mIC.commitText(text, i); @@ -226,12 +239,11 @@ public final class RichInputConnection { } public CharSequence getSelectedText(final int flags) { - if (null == mIC) return null; - return mIC.getSelectedText(flags); + return (null == mIC) ? null : mIC.getSelectedText(flags); } public boolean canDeleteCharacters() { - return mExpectedCursorPosition > 0; + return mExpectedSelStart > 0; } /** @@ -245,12 +257,12 @@ public final class RichInputConnection { * American English, it's just the most common set of rules for English). * * @param inputType a mask of the caps modes to test for. - * @param settingsValues the values of the settings to use for locale and separators. + * @param spacingAndPunctuations the values of the settings to use for locale and separators. * @param hasSpaceBefore if we should consider there should be a space after the string. * @return the caps modes that should be on as a set of bits */ - public int getCursorCapsMode(final int inputType, final SettingsValues settingsValues, - final boolean hasSpaceBefore) { + public int getCursorCapsMode(final int inputType, + final SpacingAndPunctuations spacingAndPunctuations, final boolean hasSpaceBefore) { mIC = mParent.getCurrentInputConnection(); if (null == mIC) return Constants.TextUtils.CAP_MODE_OFF; if (!TextUtils.isEmpty(mComposingText)) { @@ -268,23 +280,22 @@ public final class RichInputConnection { // heavy pressing of delete, for example DEFAULT_TEXT_CACHE_SIZE - 5 times or so. // getCapsMode should be updated to be able to return a "not enough info" result so that // we can get more context only when needed. - if (TextUtils.isEmpty(mCommittedTextBeforeComposingText) && 0 != mExpectedCursorPosition) { - final CharSequence textBeforeCursor = getTextBeforeCursor( - Constants.EDITOR_CONTENTS_CACHE_SIZE, 0); - if (!TextUtils.isEmpty(textBeforeCursor)) { - mCommittedTextBeforeComposingText.append(textBeforeCursor); + if (TextUtils.isEmpty(mCommittedTextBeforeComposingText) && 0 != mExpectedSelStart) { + if (!reloadTextCache()) { + Log.w(TAG, "Unable to connect to the editor. " + + "Setting caps mode without knowing text."); } } // This never calls InputConnection#getCapsMode - in fact, it's a static method that // never blocks or initiates IPC. return CapsModeUtils.getCapsMode(mCommittedTextBeforeComposingText, inputType, - settingsValues, hasSpaceBefore); + spacingAndPunctuations, hasSpaceBefore); } public int getCodePointBeforeCursor() { - if (mCommittedTextBeforeComposingText.length() < 1) return Constants.NOT_A_CODE; - return Character.codePointBefore(mCommittedTextBeforeComposingText, - mCommittedTextBeforeComposingText.length()); + final int length = mCommittedTextBeforeComposingText.length(); + if (length < 1) return Constants.NOT_A_CODE; + return Character.codePointBefore(mCommittedTextBeforeComposingText, length); } public CharSequence getTextBeforeCursor(final int n, final int flags) { @@ -295,8 +306,8 @@ public final class RichInputConnection { // However, if we don't have an expected cursor position, then we should always // go fetch the cache again (as it happens, INVALID_CURSOR_POSITION < 0, so we need to // test for this explicitly) - if (INVALID_CURSOR_POSITION != mExpectedCursorPosition - && (cachedLength >= n || cachedLength >= mExpectedCursorPosition)) { + if (INVALID_CURSOR_POSITION != mExpectedSelStart + && (cachedLength >= n || cachedLength >= mExpectedSelStart)) { final StringBuilder s = new StringBuilder(mCommittedTextBeforeComposingText); // We call #toString() here to create a temporary object. // In some situations, this method is called on a worker thread, and it's possible @@ -312,20 +323,19 @@ public final class RichInputConnection { return s; } mIC = mParent.getCurrentInputConnection(); - if (null != mIC) { - return mIC.getTextBeforeCursor(n, flags); - } - return null; + return (null == mIC) ? null : mIC.getTextBeforeCursor(n, flags); } public CharSequence getTextAfterCursor(final int n, final int flags) { mIC = mParent.getCurrentInputConnection(); - if (null != mIC) return mIC.getTextAfterCursor(n, flags); - return null; + return (null == mIC) ? null : mIC.getTextAfterCursor(n, flags); } public void deleteSurroundingText(final int beforeLength, final int afterLength) { if (DEBUG_BATCH_NESTING) checkBatchEdit(); + // TODO: the following is incorrect if the cursor is not immediately after the composition. + // Right now we never come here in this case because we reset the composing state before we + // come here in this case, but we need to fix this. final int remainingChars = mComposingText.length() - beforeLength; if (remainingChars >= 0) { mComposingText.setLength(remainingChars); @@ -336,16 +346,17 @@ public final class RichInputConnection { + remainingChars, 0); mCommittedTextBeforeComposingText.setLength(len); } - if (mExpectedCursorPosition > beforeLength) { - mExpectedCursorPosition -= beforeLength; + if (mExpectedSelStart > beforeLength) { + mExpectedSelStart -= beforeLength; + mExpectedSelEnd -= beforeLength; } else { - mExpectedCursorPosition = 0; + // There are fewer characters before the cursor in the buffer than we are being asked to + // delete. Only delete what is there, and update the end with the amount deleted. + mExpectedSelEnd -= mExpectedSelStart; + mExpectedSelStart = 0; } if (null != mIC) { mIC.deleteSurroundingText(beforeLength, afterLength); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.richInputConnection_deleteSurroundingText(beforeLength, afterLength); - } } if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); } @@ -354,9 +365,6 @@ public final class RichInputConnection { mIC = mParent.getCurrentInputConnection(); if (null != mIC) { mIC.performEditorAction(actionId); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.richInputConnection_performEditorAction(actionId); - } } } @@ -373,7 +381,8 @@ public final class RichInputConnection { switch (keyEvent.getKeyCode()) { case KeyEvent.KEYCODE_ENTER: mCommittedTextBeforeComposingText.append("\n"); - mExpectedCursorPosition += 1; + mExpectedSelStart += 1; + mExpectedSelEnd = mExpectedSelStart; break; case KeyEvent.KEYCODE_DEL: if (0 == mComposingText.length()) { @@ -385,26 +394,29 @@ public final class RichInputConnection { } else { mComposingText.delete(mComposingText.length() - 1, mComposingText.length()); } - if (mExpectedCursorPosition > 0) mExpectedCursorPosition -= 1; + if (mExpectedSelStart > 0 && mExpectedSelStart == mExpectedSelEnd) { + // TODO: Handle surrogate pairs. + mExpectedSelStart -= 1; + } + mExpectedSelEnd = mExpectedSelStart; break; case KeyEvent.KEYCODE_UNKNOWN: if (null != keyEvent.getCharacters()) { mCommittedTextBeforeComposingText.append(keyEvent.getCharacters()); - mExpectedCursorPosition += keyEvent.getCharacters().length(); + mExpectedSelStart += keyEvent.getCharacters().length(); + mExpectedSelEnd = mExpectedSelStart; } break; default: - final String text = new String(new int[] { keyEvent.getUnicodeChar() }, 0, 1); + final String text = StringUtils.newSingleCodePointString(keyEvent.getUnicodeChar()); mCommittedTextBeforeComposingText.append(text); - mExpectedCursorPosition += text.length(); + mExpectedSelStart += text.length(); + mExpectedSelEnd = mExpectedSelStart; break; } } if (null != mIC) { mIC.sendKeyEvent(keyEvent); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.richInputConnection_sendKeyEvent(keyEvent); - } } } @@ -415,8 +427,12 @@ public final class RichInputConnection { getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE + (end - start), 0); mCommittedTextBeforeComposingText.setLength(0); if (!TextUtils.isEmpty(textBeforeCursor)) { + // The cursor is not necessarily at the end of the composing text, but we have its + // position in mExpectedSelStart and mExpectedSelEnd. In this case we want the start + // of the text, so we should use mExpectedSelStart. In other words, the composing + // text starts (mExpectedSelStart - start) characters before the end of textBeforeCursor final int indexOfStartOfComposingText = - Math.max(textBeforeCursor.length() - (end - start), 0); + Math.max(textBeforeCursor.length() - (mExpectedSelStart - start), 0); mComposingText.append(textBeforeCursor.subSequence(indexOfStartOfComposingText, textBeforeCursor.length())); mCommittedTextBeforeComposingText.append( @@ -430,32 +446,44 @@ public final class RichInputConnection { public void setComposingText(final CharSequence text, final int newCursorPosition) { if (DEBUG_BATCH_NESTING) checkBatchEdit(); if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); - mExpectedCursorPosition += text.length() - mComposingText.length(); + mExpectedSelStart += text.length() - mComposingText.length(); + mExpectedSelEnd = mExpectedSelStart; mComposingText.setLength(0); mComposingText.append(text); - // TODO: support values of i != 1. At this time, this is never called with i != 1. + // TODO: support values of newCursorPosition != 1. At this time, this is never called with + // newCursorPosition != 1. if (null != mIC) { mIC.setComposingText(text, newCursorPosition); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.richInputConnection_setComposingText(text, newCursorPosition); - } } if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); } - public void setSelection(final int start, final int end) { + /** + * Set the selection of the text editor. + * + * Calls through to {@link InputConnection#setSelection(int, int)}. + * + * @param start the character index where the selection should start. + * @param end the character index where the selection should end. + * @return Returns true on success, false on failure: either the input connection is no longer + * valid when setting the selection or when retrieving the text cache at that point, or + * invalid arguments were passed. + */ + public boolean setSelection(final int start, final int end) { if (DEBUG_BATCH_NESTING) checkBatchEdit(); if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); + if (start < 0 || end < 0) { + return false; + } + mExpectedSelStart = start; + mExpectedSelEnd = end; if (null != mIC) { - mIC.setSelection(start, end); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.richInputConnection_setSelection(start, end); + final boolean isIcValid = mIC.setSelection(start, end); + if (!isIcValid) { + return false; } } - mExpectedCursorPosition = start; - mCommittedTextBeforeComposingText.setLength(0); - mCommittedTextBeforeComposingText.append( - getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0)); + return reloadTextCache(); } public void commitCorrection(final CorrectionInfo correctionInfo) { @@ -476,26 +504,30 @@ public final class RichInputConnection { // text should never be null, but just in case, it's better to insert nothing than to crash if (null == text) text = ""; mCommittedTextBeforeComposingText.append(text); - mExpectedCursorPosition += text.length() - mComposingText.length(); + mExpectedSelStart += text.length() - mComposingText.length(); + mExpectedSelEnd = mExpectedSelStart; mComposingText.setLength(0); if (null != mIC) { mIC.commitCompletion(completionInfo); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.richInputConnection_commitCompletion(completionInfo); - } } if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); } @SuppressWarnings("unused") - public String getNthPreviousWord(final String sentenceSeperators, final int n) { + public PrevWordsInfo getPrevWordsInfoFromNthPreviousWord( + final SpacingAndPunctuations spacingAndPunctuations, final int n) { mIC = mParent.getCurrentInputConnection(); - if (null == mIC) return null; + if (null == mIC) { + return PrevWordsInfo.EMPTY_PREV_WORDS_INFO; + } final CharSequence prev = getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0); if (DEBUG_PREVIOUS_TEXT && null != prev) { final int checkLength = LOOKBACK_CHARACTER_NUM - 1; final String reference = prev.length() <= checkLength ? prev.toString() : prev.subSequence(prev.length() - checkLength, prev.length()).toString(); + // TODO: right now the following works because mComposingText holds the part of the + // composing text that is before the cursor, but this is very confusing. We should + // fix it. final StringBuilder internal = new StringBuilder() .append(mCommittedTextBeforeComposingText).append(mComposingText); if (internal.length() > checkLength) { @@ -507,71 +539,24 @@ public final class RichInputConnection { } } } - return getNthPreviousWord(prev, sentenceSeperators, n); - } - - private static boolean isSeparator(int code, String sep) { - return sep.indexOf(code) != -1; + return PrevWordsInfoUtils.getPrevWordsInfoFromNthPreviousWord( + prev, spacingAndPunctuations, n); } - // 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 String getNthPreviousWord(final CharSequence prev, - final String sentenceSeperators, final int n) { - if (prev == null) return null; - final String[] w = spaceRegex.split(prev); - - // If we can't find n words, or we found an empty word, return null. - if (w.length < n) return null; - final String nthPrevWord = w[w.length - n]; - final int length = nthPrevWord.length(); - if (length <= 0) return null; - - // If ends in a separator, return null - final char lastChar = nthPrevWord.charAt(length - 1); - if (sentenceSeperators.contains(String.valueOf(lastChar))) return null; - - return nthPrevWord; - } - - /** - * @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 CharSequence getWordAtCursor(String separators) { - // getWordRangeAtCursor returns null if the connection is null - TextRange r = getWordRangeAtCursor(separators, 0); - return (r == null) ? null : r.mWord; + private static boolean isSeparator(final int code, final int[] sortedSeparators) { + return Arrays.binarySearch(sortedSeparators, code) >= 0; } /** * 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 + * @param sortedSeparators a sorted array of code points that split words. + * @param scriptId the script we consider to be writing words, as one of ScriptUtils.SCRIPT_* * @return a range containing the text surrounding the cursor */ - public TextRange getWordRangeAtCursor(final String sep, - final int additionalPrecedingWordsCount) { + public TextRange getWordRangeAtCursor(final int[] sortedSeparators, final int scriptId) { mIC = mParent.getCurrentInputConnection(); - if (mIC == null || sep == null) { + if (mIC == null) { return null; } final CharSequence before = mIC.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, @@ -582,36 +567,26 @@ public final class RichInputConnection { return null; } - // Going backward, alternate skipping non-separators and separators until enough words - // have been read. - int count = additionalPrecedingWordsCount; + // Going backward, find the first breaking point (separator) int startIndexInBefore = 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 (startIndexInBefore > 0) { - final int codePoint = Character.codePointBefore(before, startIndexInBefore); - if (isStoppingAtWhitespace == isSeparator(codePoint, sep)) { - break; // inner loop - } - --startIndexInBefore; - if (Character.isSupplementaryCodePoint(codePoint)) { - --startIndexInBefore; - } + while (startIndexInBefore > 0) { + final int codePoint = Character.codePointBefore(before, startIndexInBefore); + if (isSeparator(codePoint, sortedSeparators) + || !ScriptUtils.isLetterPartOfScript(codePoint, scriptId)) { + break; } - // isStoppingAtWhitespace is true every other time through the loop, - // so additionalPrecedingWordsCount is guaranteed to become < 0, which - // guarantees outer loop termination - if (isStoppingAtWhitespace && (--count < 0)) { - break; // outer loop + --startIndexInBefore; + if (Character.isSupplementaryCodePoint(codePoint)) { + --startIndexInBefore; } - isStoppingAtWhitespace = !isStoppingAtWhitespace; } // Find last word separator after the cursor int endIndexInAfter = -1; while (++endIndexInAfter < after.length()) { final int codePoint = Character.codePointAt(after, endIndexInAfter); - if (isSeparator(codePoint, sep)) { + if (isSeparator(codePoint, sortedSeparators) + || !ScriptUtils.isLetterPartOfScript(codePoint, scriptId)) { break; } if (Character.isSupplementaryCodePoint(codePoint)) { @@ -619,27 +594,50 @@ public final class RichInputConnection { } } + final boolean hasUrlSpans = + SpannableStringUtils.hasUrlSpans(before, startIndexInBefore, before.length()) + || SpannableStringUtils.hasUrlSpans(after, 0, endIndexInAfter); // We don't use TextUtils#concat because it copies all spans without respect to their // nature. If the text includes a PARAGRAPH span and it has been split, then // TextUtils#concat will crash when it tries to concat both sides of it. return new TextRange( SpannableStringUtils.concatWithNonParagraphSuggestionSpansOnly(before, after), - startIndexInBefore, before.length() + endIndexInAfter, before.length()); + startIndexInBefore, before.length() + endIndexInAfter, before.length(), + hasUrlSpans); } - public boolean isCursorTouchingWord(final SettingsValues settingsValues) { - final int codePointBeforeCursor = getCodePointBeforeCursor(); - if (Constants.NOT_A_CODE != codePointBeforeCursor - && !settingsValues.isWordSeparator(codePointBeforeCursor) - && !settingsValues.isWordConnector(codePointBeforeCursor)) { + public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations) { + if (isCursorFollowedByWordCharacter(spacingAndPunctuations)) { + // If what's after the cursor is a word character, then we're touching a word. return true; } + final String textBeforeCursor = mCommittedTextBeforeComposingText.toString(); + int indexOfCodePointInJavaChars = textBeforeCursor.length(); + int consideredCodePoint = 0 == indexOfCodePointInJavaChars ? Constants.NOT_A_CODE + : textBeforeCursor.codePointBefore(indexOfCodePointInJavaChars); + // Search for the first non word-connector char + if (spacingAndPunctuations.isWordConnector(consideredCodePoint)) { + indexOfCodePointInJavaChars -= Character.charCount(consideredCodePoint); + consideredCodePoint = 0 == indexOfCodePointInJavaChars ? Constants.NOT_A_CODE + : textBeforeCursor.codePointBefore(indexOfCodePointInJavaChars); + } + return !(Constants.NOT_A_CODE == consideredCodePoint + || spacingAndPunctuations.isWordSeparator(consideredCodePoint) + || spacingAndPunctuations.isWordConnector(consideredCodePoint)); + } + + public boolean isCursorFollowedByWordCharacter( + final SpacingAndPunctuations spacingAndPunctuations) { final CharSequence after = getTextAfterCursor(1, 0); - if (!TextUtils.isEmpty(after) && !settingsValues.isWordSeparator(after.charAt(0)) - && !settingsValues.isWordConnector(after.charAt(0))) { - return true; + if (TextUtils.isEmpty(after)) { + return false; + } + final int codePointAfterCursor = Character.codePointAt(after, 0); + if (spacingAndPunctuations.isWordSeparator(codePointAfterCursor) + || spacingAndPunctuations.isWordConnector(codePointAfterCursor)) { + return false; } - return false; + return true; } public void removeTrailingSpace() { @@ -655,57 +653,17 @@ public final class RichInputConnection { return TextUtils.equals(text, beforeText); } - /* (non-javadoc) - * Returns the word before the cursor if the cursor is at the end of a word, null otherwise - */ - public CharSequence getWordBeforeCursorIfAtEndOfWord(final SettingsValues settings) { - // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace, - // separator or end of line/text) - // Example: "test|"<EOL> "te|st" get rejected here - final CharSequence textAfterCursor = getTextAfterCursor(1, 0); - if (!TextUtils.isEmpty(textAfterCursor) - && !settings.isWordSeparator(textAfterCursor.charAt(0))) return null; - - // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe) - // Example: " -|" gets rejected here but "e-|" and "e|" are okay - CharSequence word = getWordAtCursor(settings.mWordSeparators); - // We don't suggest on leading single quotes, so we have to remove them from the word if - // it starts with single quotes. - while (!TextUtils.isEmpty(word) && Constants.CODE_SINGLE_QUOTE == word.charAt(0)) { - word = word.subSequence(1, word.length()); - } - if (TextUtils.isEmpty(word)) return null; - // Find the last code point of the string - final int lastCodePoint = Character.codePointBefore(word, word.length()); - // If for some reason the text field contains non-unicode binary data, or if the - // charsequence is exactly one char long and the contents is a low surrogate, return null. - if (!Character.isDefined(lastCodePoint)) return null; - // Bail out if the cursor is not at the end of a word (cursor must be preceded by - // non-whitespace, non-separator, non-start-of-text) - // Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here. - if (settings.isWordSeparator(lastCodePoint)) return null; - final char firstChar = word.charAt(0); // we just tested that word is not empty - if (word.length() == 1 && !Character.isLetter(firstChar)) return null; - - // We don't restart suggestion if the first character is not a letter, because we don't - // start composing when the first character is not a letter. - if (!Character.isLetter(firstChar)) return null; - - return word; - } - public boolean revertDoubleSpacePeriod() { if (DEBUG_BATCH_NESTING) checkBatchEdit(); // Here we test whether we indeed have a period and a space before us. This should not // be needed, but it's there just in case something went wrong. final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0); - final String periodSpace = ". "; - if (!TextUtils.equals(periodSpace, textBeforeCursor)) { + if (!TextUtils.equals(Constants.STRING_PERIOD_AND_SPACE, 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 " - + "\"" + periodSpace + "\" just before the cursor."); + + "\"" + Constants.STRING_PERIOD_AND_SPACE + "\" just before the cursor."); return false; } // Double-space results in ". ". A backspace to cancel this should result in a single @@ -713,9 +671,6 @@ public final class RichInputConnection { deleteSurroundingText(2, 0); final String singleSpace = " "; commitText(singleSpace, 1); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.richInputConnection_revertDoubleSpacePeriod(); - } return true; } @@ -738,9 +693,6 @@ public final class RichInputConnection { deleteSurroundingText(2, 0); final String text = " " + textBeforeCursor.subSequence(0, 1); commitText(text, 1); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.richInputConnection_revertSwapPunctuation(); - } return true; } @@ -758,20 +710,30 @@ public final class RichInputConnection { * this update and not the ones in-between. This is almost impossible to achieve even trying * very very hard. * - * @param oldSelStart The value of the old cursor position in the update. - * @param newSelStart The value of the new cursor position in the update. + * @param oldSelStart The value of the old selection in the update. + * @param newSelStart The value of the new selection in the update. + * @param oldSelEnd The value of the old selection end in the update. + * @param newSelEnd The value of the new selection end in the update. * @return whether this is a belated expected update or not. */ - public boolean isBelatedExpectedUpdate(final int oldSelStart, final int newSelStart) { - // If this is an update that arrives at our expected position, it's a belated update. - if (newSelStart == mExpectedCursorPosition) return true; - // If this is an update that moves the cursor from our expected position, it must be - // an explicit move. - if (oldSelStart == mExpectedCursorPosition) return false; - // The following returns true if newSelStart is between oldSelStart and - // mCurrentCursorPosition. We assume that if the updated position is between the old - // position and the expected position, then it must be a belated update. - return (newSelStart - oldSelStart) * (mExpectedCursorPosition - newSelStart) >= 0; + public boolean isBelatedExpectedUpdate(final int oldSelStart, final int newSelStart, + final int oldSelEnd, final int newSelEnd) { + // This update is "belated" if we are expecting it. That is, mExpectedSelStart and + // mExpectedSelEnd match the new values that the TextView is updating TO. + if (mExpectedSelStart == newSelStart && mExpectedSelEnd == newSelEnd) return true; + // This update is not belated if mExpectedSelStart and mExpectedSelEnd match the old + // values, and one of newSelStart or newSelEnd is updated to a different value. In this + // case, it is likely that something other than the IME has moved the selection endpoint + // to the new value. + if (mExpectedSelStart == oldSelStart && mExpectedSelEnd == oldSelEnd + && (oldSelStart != newSelStart || oldSelEnd != newSelEnd)) return false; + // If neither of the above two cases hold, then the system may be having trouble keeping up + // with updates. If 1) the selection is a cursor, 2) newSelStart is between oldSelStart + // and mExpectedSelStart, and 3) newSelEnd is between oldSelEnd and mExpectedSelEnd, then + // assume a belated update. + return (newSelStart == newSelEnd) + && (newSelStart - oldSelStart) * (mExpectedSelStart - newSelStart) >= 0 + && (newSelEnd - oldSelEnd) * (mExpectedSelEnd - newSelEnd) >= 0; } /** @@ -784,4 +746,69 @@ public final class RichInputConnection { public boolean textBeforeCursorLooksLikeURL() { return StringUtils.lastPartLooksLikeURL(mCommittedTextBeforeComposingText); } + + /** + * Looks at the text just before the cursor to find out if we are inside a double quote. + * + * As with #textBeforeCursorLooksLikeURL, this is dependent on how much text we have cached. + * However this won't be a concrete problem in most situations, as the cache is almost always + * long enough for this use. + */ + public boolean isInsideDoubleQuoteOrAfterDigit() { + return StringUtils.isInsideDoubleQuoteOrAfterDigit(mCommittedTextBeforeComposingText); + } + + /** + * Try to get the text from the editor to expose lies the framework may have been + * telling us. Concretely, when the device rotates, the frameworks tells us about where the + * cursor used to be initially in the editor at the time it first received the focus; this + * may be completely different from the place it is upon rotation. Since we don't have any + * means to get the real value, try at least to ask the text view for some characters and + * detect the most damaging cases: when the cursor position is declared to be much smaller + * than it really is. + */ + public void tryFixLyingCursorPosition() { + final CharSequence textBeforeCursor = getTextBeforeCursor( + Constants.EDITOR_CONTENTS_CACHE_SIZE, 0); + if (null == textBeforeCursor) { + mExpectedSelStart = mExpectedSelEnd = Constants.NOT_A_CURSOR_POSITION; + } else { + final int textLength = textBeforeCursor.length(); + if (textLength < Constants.EDITOR_CONTENTS_CACHE_SIZE + && (textLength > mExpectedSelStart + || mExpectedSelStart < Constants.EDITOR_CONTENTS_CACHE_SIZE)) { + // It should not be possible to have only one of those variables be + // NOT_A_CURSOR_POSITION, so if they are equal, either the selection is zero-sized + // (simple cursor, no selection) or there is no cursor/we don't know its pos + final boolean wasEqual = mExpectedSelStart == mExpectedSelEnd; + mExpectedSelStart = textLength; + // We can't figure out the value of mLastSelectionEnd :( + // But at least if it's smaller than mLastSelectionStart something is wrong, + // and if they used to be equal we also don't want to make it look like there is a + // selection. + if (wasEqual || mExpectedSelStart > mExpectedSelEnd) { + mExpectedSelEnd = mExpectedSelStart; + } + } + } + } + + public int getExpectedSelectionStart() { + return mExpectedSelStart; + } + + public int getExpectedSelectionEnd() { + return mExpectedSelEnd; + } + + /** + * @return whether there is a selection currently active. + */ + public boolean hasSelection() { + return mExpectedSelEnd != mExpectedSelStart; + } + + public boolean isCursorPositionKnown() { + return INVALID_CURSOR_POSITION != mExpectedSelStart; + } } diff --git a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java index 6b6bbf3a7..7cf4eff92 100644 --- a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java +++ b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java @@ -20,6 +20,7 @@ import static com.android.inputmethod.latin.Constants.Subtype.KEYBOARD_MODE; import android.content.Context; import android.content.SharedPreferences; +import android.os.Build; import android.os.IBinder; import android.preference.PreferenceManager; import android.util.Log; @@ -30,7 +31,6 @@ import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.compat.InputMethodManagerCompatWrapper; import com.android.inputmethod.latin.settings.Settings; import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils; -import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; import java.util.Collections; @@ -50,11 +50,11 @@ public final class RichInputMethodManager { private static final RichInputMethodManager sInstance = new RichInputMethodManager(); private InputMethodManagerCompatWrapper mImmWrapper; - private InputMethodInfo mInputMethodInfoOfThisIme; + private InputMethodInfoCache mInputMethodInfoCache; final HashMap<InputMethodInfo, List<InputMethodSubtype>> - mSubtypeListCacheWithImplicitlySelectedSubtypes = CollectionUtils.newHashMap(); + mSubtypeListCacheWithImplicitlySelectedSubtypes = new HashMap<>(); final HashMap<InputMethodInfo, List<InputMethodSubtype>> - mSubtypeListCacheWithoutImplicitlySelectedSubtypes = CollectionUtils.newHashMap(); + mSubtypeListCacheWithoutImplicitlySelectedSubtypes = new HashMap<>(); private static final int INDEX_NOT_FOUND = -1; @@ -64,8 +64,7 @@ public final class RichInputMethodManager { } public static void init(final Context context) { - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - sInstance.initInternal(context, prefs); + sInstance.initInternal(context); } private boolean isInitialized() { @@ -78,20 +77,26 @@ public final class RichInputMethodManager { } } - private void initInternal(final Context context, final SharedPreferences prefs) { + private void initInternal(final Context context) { if (isInitialized()) { return; } mImmWrapper = new InputMethodManagerCompatWrapper(context); - mInputMethodInfoOfThisIme = getInputMethodInfoOfThisIme(context); + mInputMethodInfoCache = new InputMethodInfoCache( + mImmWrapper.mImm, context.getPackageName()); // Initialize additional subtypes. SubtypeLocaleUtils.init(context); + final InputMethodSubtype[] additionalSubtypes = getAdditionalSubtypes(context); + setAdditionalInputMethodSubtypes(additionalSubtypes); + } + + public InputMethodSubtype[] getAdditionalSubtypes(final Context context) { + SubtypeLocaleUtils.init(context); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); final String prefAdditionalSubtypes = Settings.readPrefAdditionalSubtypes( prefs, context.getResources()); - final InputMethodSubtype[] additionalSubtypes = - AdditionalSubtypeUtils.createAdditionalSubtypesArray(prefAdditionalSubtypes); - setAdditionalInputMethodSubtypes(additionalSubtypes); + return AdditionalSubtypeUtils.createAdditionalSubtypesArray(prefAdditionalSubtypes); } public InputMethodManager getInputMethodManager() { @@ -99,20 +104,10 @@ public final class RichInputMethodManager { return mImmWrapper.mImm; } - private InputMethodInfo getInputMethodInfoOfThisIme(final Context context) { - final String packageName = context.getPackageName(); - for (final InputMethodInfo imi : mImmWrapper.mImm.getInputMethodList()) { - if (imi.getPackageName().equals(packageName)) { - return imi; - } - } - throw new RuntimeException("Input method id for " + packageName + " not found."); - } - public List<InputMethodSubtype> getMyEnabledInputMethodSubtypeList( boolean allowsImplicitlySelectedSubtypes) { - return getEnabledInputMethodSubtypeList(mInputMethodInfoOfThisIme, - allowsImplicitlySelectedSubtypes); + return getEnabledInputMethodSubtypeList( + getInputMethodInfoOfThisIme(), allowsImplicitlySelectedSubtypes); } public boolean switchToNextInputMethod(final IBinder token, final boolean onlyCurrentIme) { @@ -153,10 +148,10 @@ public final class RichInputMethodManager { private boolean switchToNextInputMethodAndSubtype(final IBinder token) { final InputMethodManager imm = mImmWrapper.mImm; final List<InputMethodInfo> enabledImis = imm.getEnabledInputMethodList(); - final int currentIndex = getImiIndexInList(mInputMethodInfoOfThisIme, enabledImis); + final int currentIndex = getImiIndexInList(getInputMethodInfoOfThisIme(), enabledImis); if (currentIndex == INDEX_NOT_FOUND) { Log.w(TAG, "Can't find current IME in enabled IMEs: IME package=" - + mInputMethodInfoOfThisIme.getPackageName()); + + getInputMethodInfoOfThisIme().getPackageName()); return false; } final InputMethodInfo nextImi = getNextNonAuxiliaryIme(currentIndex, enabledImis); @@ -213,16 +208,45 @@ public final class RichInputMethodManager { return true; } + private static class InputMethodInfoCache { + private final InputMethodManager mImm; + private final String mImePackageName; + + private InputMethodInfo mCachedValue; + + public InputMethodInfoCache(final InputMethodManager imm, final String imePackageName) { + mImm = imm; + mImePackageName = imePackageName; + } + + public synchronized InputMethodInfo get() { + if (mCachedValue != null) { + return mCachedValue; + } + for (final InputMethodInfo imi : mImm.getInputMethodList()) { + if (imi.getPackageName().equals(mImePackageName)) { + mCachedValue = imi; + return imi; + } + } + throw new RuntimeException("Input method id for " + mImePackageName + " not found."); + } + + public synchronized void clear() { + mCachedValue = null; + } + } + public InputMethodInfo getInputMethodInfoOfThisIme() { - return mInputMethodInfoOfThisIme; + return mInputMethodInfoCache.get(); } public String getInputMethodIdOfThisIme() { - return mInputMethodInfoOfThisIme.getId(); + return getInputMethodInfoOfThisIme().getId(); } public boolean checkIfSubtypeBelongsToThisImeAndEnabled(final InputMethodSubtype subtype) { - return checkIfSubtypeBelongsToImeAndEnabled(mInputMethodInfoOfThisIme, subtype); + return checkIfSubtypeBelongsToImeAndEnabled(getInputMethodInfoOfThisIme(), subtype); } public boolean checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled( @@ -258,7 +282,7 @@ public final class RichInputMethodManager { } public boolean checkIfSubtypeBelongsToThisIme(final InputMethodSubtype subtype) { - return getSubtypeIndexInIme(subtype, mInputMethodInfoOfThisIme) != INDEX_NOT_FOUND; + return getSubtypeIndexInIme(subtype, getInputMethodInfoOfThisIme()) != INDEX_NOT_FOUND; } private static int getSubtypeIndexInIme(final InputMethodSubtype subtype, @@ -286,7 +310,8 @@ public final class RichInputMethodManager { public boolean hasMultipleEnabledSubtypesInThisIme( final boolean shouldIncludeAuxiliarySubtypes) { - final List<InputMethodInfo> imiList = Collections.singletonList(mInputMethodInfoOfThisIme); + final List<InputMethodInfo> imiList = Collections.singletonList( + getInputMethodInfoOfThisIme()); return hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, imiList); } @@ -340,7 +365,7 @@ public final class RichInputMethodManager { public InputMethodSubtype findSubtypeByLocaleAndKeyboardLayoutSet(final String localeString, final String keyboardLayoutSetName) { - final InputMethodInfo myImi = mInputMethodInfoOfThisIme; + final InputMethodInfo myImi = getInputMethodInfoOfThisIme(); final int count = myImi.getSubtypeCount(); for (int i = 0; i < count; i++) { final InputMethodSubtype subtype = myImi.getSubtypeAt(i); @@ -355,13 +380,14 @@ public final class RichInputMethodManager { public void setInputMethodAndSubtype(final IBinder token, final InputMethodSubtype subtype) { mImmWrapper.mImm.setInputMethodAndSubtype( - token, mInputMethodInfoOfThisIme.getId(), subtype); + token, getInputMethodIdOfThisIme(), subtype); } public void setAdditionalInputMethodSubtypes(final InputMethodSubtype[] subtypes) { mImmWrapper.mImm.setAdditionalInputMethodSubtypes( - mInputMethodInfoOfThisIme.getId(), subtypes); - // Clear the cache so that we go read the subtypes again next time. + getInputMethodIdOfThisIme(), subtypes); + // Clear the cache so that we go read the {@link InputMethodInfo} of this IME and list of + // subtypes again next time. clearSubtypeCaches(); } @@ -382,5 +408,17 @@ public final class RichInputMethodManager { public void clearSubtypeCaches() { mSubtypeListCacheWithImplicitlySelectedSubtypes.clear(); mSubtypeListCacheWithoutImplicitlySelectedSubtypes.clear(); + mInputMethodInfoCache.clear(); + } + + public boolean shouldOfferSwitchingToNextInputMethod(final IBinder binder, + boolean defaultValue) { + // Use the default value instead on Jelly Bean MR2 and previous where + // {@link InputMethodManager#shouldOfferSwitchingToNextInputMethod} isn't yet available + // and on KitKat where the API is still just a stub to return true always. + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { + return defaultValue; + } + return mImmWrapper.shouldOfferSwitchingToNextInputMethod(binder); } } diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java index cd9c89f04..a3d09565c 100644 --- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java +++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java @@ -32,12 +32,17 @@ import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils; import com.android.inputmethod.keyboard.KeyboardSwitcher; +import com.android.inputmethod.keyboard.internal.LanguageOnSpacebarHelper; +import com.android.inputmethod.latin.utils.LocaleUtils; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; public final class SubtypeSwitcher { private static boolean DBG = LatinImeLogger.sDBG; @@ -47,49 +52,43 @@ public final class SubtypeSwitcher { private /* final */ RichInputMethodManager mRichImm; private /* final */ Resources mResources; - private /* final */ ConnectivityManager mConnectivityManager; - private final NeedsToDisplayLanguage mNeedsToDisplayLanguage = new NeedsToDisplayLanguage(); + private final LanguageOnSpacebarHelper mLanguageOnSpacebarHelper = + new LanguageOnSpacebarHelper(); private InputMethodInfo mShortcutInputMethodInfo; private InputMethodSubtype mShortcutSubtype; private InputMethodSubtype mNoLanguageSubtype; private InputMethodSubtype mEmojiSubtype; private boolean mIsNetworkConnected; + private static final String KEYBOARD_MODE = "keyboard"; // Dummy no language QWERTY subtype. See {@link R.xml.method}. - private static final InputMethodSubtype DUMMY_NO_LANGUAGE_SUBTYPE = new InputMethodSubtype( - R.string.subtype_no_language_qwerty, R.drawable.ic_ime_switcher_dark, - SubtypeLocaleUtils.NO_LANGUAGE, "keyboard", "KeyboardLayoutSet=" - + SubtypeLocaleUtils.QWERTY - + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE - + ",EnabledWhenDefaultIsNotAsciiCapable," - + Constants.Subtype.ExtraValue.EMOJI_CAPABLE, - false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */); + private static final int SUBTYPE_ID_OF_DUMMY_NO_LANGUAGE_SUBTYPE = 0xdde0bfd3; + private static final String EXTRA_VALUE_OF_DUMMY_NO_LANGUAGE_SUBTYPE = + "KeyboardLayoutSet=" + SubtypeLocaleUtils.QWERTY + + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE + + "," + Constants.Subtype.ExtraValue.ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE + + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE; + private static final InputMethodSubtype DUMMY_NO_LANGUAGE_SUBTYPE = + InputMethodSubtypeCompatUtils.newInputMethodSubtype( + R.string.subtype_no_language_qwerty, R.drawable.ic_ime_switcher_dark, + SubtypeLocaleUtils.NO_LANGUAGE, KEYBOARD_MODE, + EXTRA_VALUE_OF_DUMMY_NO_LANGUAGE_SUBTYPE, + false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */, + SUBTYPE_ID_OF_DUMMY_NO_LANGUAGE_SUBTYPE); // Caveat: We probably should remove this when we add an Emoji subtype in {@link R.xml.method}. // Dummy Emoji subtype. See {@link R.xml.method}. - private static final InputMethodSubtype DUMMY_EMOJI_SUBTYPE = new InputMethodSubtype( - R.string.subtype_emoji, R.drawable.ic_ime_switcher_dark, - SubtypeLocaleUtils.NO_LANGUAGE, "keyboard", "KeyboardLayoutSet=" - + SubtypeLocaleUtils.EMOJI + "," - + Constants.Subtype.ExtraValue.EMOJI_CAPABLE, - false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */); - - static final class NeedsToDisplayLanguage { - private int mEnabledSubtypeCount; - private boolean mIsSystemLanguageSameAsInputLanguage; - - public boolean getValue() { - return mEnabledSubtypeCount >= 2 || !mIsSystemLanguageSameAsInputLanguage; - } - - public void updateEnabledSubtypeCount(final int count) { - mEnabledSubtypeCount = count; - } - - public void updateIsSystemLanguageSameAsInputLanguage(final boolean isSame) { - mIsSystemLanguageSameAsInputLanguage = isSame; - } - } + private static final int SUBTYPE_ID_OF_DUMMY_EMOJI_SUBTYPE = 0xd78b2ed0; + private static final String EXTRA_VALUE_OF_DUMMY_EMOJI_SUBTYPE = + "KeyboardLayoutSet=" + SubtypeLocaleUtils.EMOJI + + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE; + private static final InputMethodSubtype DUMMY_EMOJI_SUBTYPE = + InputMethodSubtypeCompatUtils.newInputMethodSubtype( + R.string.subtype_emoji, R.drawable.ic_ime_switcher_dark, + SubtypeLocaleUtils.NO_LANGUAGE, KEYBOARD_MODE, + EXTRA_VALUE_OF_DUMMY_EMOJI_SUBTYPE, + false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */, + SUBTYPE_ID_OF_DUMMY_EMOJI_SUBTYPE); public static SubtypeSwitcher getInstance() { return sInstance; @@ -111,10 +110,10 @@ public final class SubtypeSwitcher { } mResources = context.getResources(); mRichImm = RichInputMethodManager.getInstance(); - mConnectivityManager = (ConnectivityManager) context.getSystemService( + ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService( Context.CONNECTIVITY_SERVICE); - final NetworkInfo info = mConnectivityManager.getActiveNetworkInfo(); + final NetworkInfo info = connectivityManager.getActiveNetworkInfo(); mIsNetworkConnected = (info != null && info.isConnected()); onSubtypeChanged(getCurrentSubtype()); @@ -128,7 +127,7 @@ public final class SubtypeSwitcher { public void updateParametersOnStartInputView() { final List<InputMethodSubtype> enabledSubtypesOfThisIme = mRichImm.getMyEnabledInputMethodSubtypeList(true); - mNeedsToDisplayLanguage.updateEnabledSubtypeCount(enabledSubtypesOfThisIme.size()); + mLanguageOnSpacebarHelper.updateEnabledSubtypes(enabledSubtypesOfThisIme); updateShortcutIME(); } @@ -177,7 +176,7 @@ public final class SubtypeSwitcher { final boolean sameLanguage = systemLocale.getLanguage().equals(newLocale.getLanguage()); final boolean implicitlyEnabled = mRichImm.checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(newSubtype); - mNeedsToDisplayLanguage.updateIsSystemLanguageSameAsInputLanguage( + mLanguageOnSpacebarHelper.updateIsSystemLanguageSameAsInputLanguage( sameLocale || (sameLanguage && implicitlyEnabled)); updateShortcutIME(); @@ -213,6 +212,7 @@ public final class SubtypeSwitcher { } public boolean isShortcutImeEnabled() { + updateShortcutIME(); if (mShortcutInputMethodInfo == null) { return false; } @@ -224,10 +224,13 @@ public final class SubtypeSwitcher { } public boolean isShortcutImeReady() { - if (mShortcutInputMethodInfo == null) + updateShortcutIME(); + if (mShortcutInputMethodInfo == null) { return false; - if (mShortcutSubtype == null) + } + if (mShortcutSubtype == null) { return true; + } if (mShortcutSubtype.containsExtraValueKey(REQ_NETWORK_CONNECTIVITY)) { return mIsNetworkConnected; } @@ -246,28 +249,52 @@ public final class SubtypeSwitcher { // Subtype Switching functions // ////////////////////////////////// - public boolean needsToDisplayLanguage(final Locale keyboardLocale) { - if (keyboardLocale.toString().equals(SubtypeLocaleUtils.NO_LANGUAGE)) { - return true; + public int getLanguageOnSpacebarFormatType(final InputMethodSubtype subtype) { + return mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(subtype); + } + + public boolean isSystemLocaleSameAsLocaleOfAllEnabledSubtypesOfEnabledImes() { + final Locale systemLocale = mResources.getConfiguration().locale; + final Set<InputMethodSubtype> enabledSubtypesOfEnabledImes = new HashSet<>(); + final InputMethodManager inputMethodManager = mRichImm.getInputMethodManager(); + final List<InputMethodInfo> enabledInputMethodInfoList = + inputMethodManager.getEnabledInputMethodList(); + for (final InputMethodInfo info : enabledInputMethodInfoList) { + final List<InputMethodSubtype> enabledSubtypes = + inputMethodManager.getEnabledInputMethodSubtypeList( + info, true /* allowsImplicitlySelectedSubtypes */); + if (enabledSubtypes.isEmpty()) { + // An IME with no subtypes is found. + return false; + } + enabledSubtypesOfEnabledImes.addAll(enabledSubtypes); } - if (!keyboardLocale.equals(getCurrentSubtypeLocale())) { - return false; + for (final InputMethodSubtype subtype : enabledSubtypesOfEnabledImes) { + if (!subtype.isAuxiliary() && !subtype.getLocale().isEmpty() + && !systemLocale.equals(SubtypeLocaleUtils.getSubtypeLocale(subtype))) { + return false; + } } - return mNeedsToDisplayLanguage.getValue(); + return true; } - private static Locale sForcedLocaleForTesting = null; + private static InputMethodSubtype sForcedSubtypeForTesting = null; @UsedForTesting - void forceLocale(final Locale locale) { - sForcedLocaleForTesting = locale; + void forceSubtype(final InputMethodSubtype subtype) { + sForcedSubtypeForTesting = subtype; } public Locale getCurrentSubtypeLocale() { - if (null != sForcedLocaleForTesting) return sForcedLocaleForTesting; + if (null != sForcedSubtypeForTesting) { + return LocaleUtils.constructLocaleFromString(sForcedSubtypeForTesting.getLocale()); + } return SubtypeLocaleUtils.getSubtypeLocale(getCurrentSubtype()); } public InputMethodSubtype getCurrentSubtype() { + if (null != sForcedSubtypeForTesting) { + return sForcedSubtypeForTesting; + } return mRichImm.getCurrentInputMethodSubtype(getNoLanguageSubtype()); } @@ -279,8 +306,8 @@ public final class SubtypeSwitcher { if (mNoLanguageSubtype != null) { return mNoLanguageSubtype; } - Log.w(TAG, "Can't find no lanugage with QWERTY subtype"); - Log.w(TAG, "No input method subtype found; return dummy subtype: " + Log.w(TAG, "Can't find any language with QWERTY subtype"); + Log.w(TAG, "No input method subtype found; returning dummy subtype: " + DUMMY_NO_LANGUAGE_SUBTYPE); return DUMMY_NO_LANGUAGE_SUBTYPE; } @@ -293,8 +320,13 @@ public final class SubtypeSwitcher { if (mEmojiSubtype != null) { return mEmojiSubtype; } - Log.w(TAG, "Can't find Emoji subtype"); - Log.w(TAG, "No input method subtype found; return dummy subtype: " + DUMMY_EMOJI_SUBTYPE); + Log.w(TAG, "Can't find emoji subtype"); + Log.w(TAG, "No input method subtype found; returning dummy subtype: " + + DUMMY_EMOJI_SUBTYPE); return DUMMY_EMOJI_SUBTYPE; } + + public String getCombiningRulesExtraValueOfCurrentSubtype() { + return SubtypeLocaleUtils.getCombiningRulesExtraValue(getCurrentSubtype()); + } } diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index 0a4c7a55d..c347f69a9 100644 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -16,28 +16,18 @@ package com.android.inputmethod.latin; -import android.content.Context; -import android.preference.PreferenceManager; import android.text.TextUtils; -import android.util.Log; -import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; -import com.android.inputmethod.latin.personalization.PersonalizationDictionary; -import com.android.inputmethod.latin.personalization.PersonalizationPredictionDictionary; -import com.android.inputmethod.latin.personalization.UserHistoryDictionary; -import com.android.inputmethod.latin.settings.Settings; +import com.android.inputmethod.latin.define.ProductionFlag; import com.android.inputmethod.latin.utils.AutoCorrectionUtils; -import com.android.inputmethod.latin.utils.BoundedTreeSet; -import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.latin.utils.BinaryDictionaryUtils; import com.android.inputmethod.latin.utils.StringUtils; +import com.android.inputmethod.latin.utils.SuggestionResults; import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashSet; import java.util.Locale; -import java.util.concurrent.ConcurrentHashMap; /** * This class loads a dictionary and provides a list of suggestions for a given sequence of @@ -60,153 +50,20 @@ public final class Suggest { // Close to -2**31 private static final int SUPPRESS_SUGGEST_THRESHOLD = -2000000000; - public static final int MAX_SUGGESTIONS = 18; - - public interface SuggestInitializationListener { - public void onUpdateMainDictionaryAvailability(boolean isMainDictionaryAvailable); - } - private static final boolean DBG = LatinImeLogger.sDBG; - - private final ConcurrentHashMap<String, Dictionary> mDictionaries = - CollectionUtils.newConcurrentHashMap(); - private HashSet<String> mOnlyDictionarySetForDebug = null; - private Dictionary mMainDictionary; - private ContactsBinaryDictionary mContactsDict; - @UsedForTesting - private boolean mIsCurrentlyWaitingForMainDictionary = false; + private final DictionaryFacilitator mDictionaryFacilitator; private float mAutoCorrectionThreshold; - // Locale used for upper- and title-casing words - public final Locale mLocale; - - public Suggest(final Context context, final Locale locale, - final SuggestInitializationListener listener) { - initAsynchronously(context, locale, listener); - mLocale = locale; - // initialize a debug flag for the personalization - if (Settings.readUseOnlyPersonalizationDictionaryForDebug( - PreferenceManager.getDefaultSharedPreferences(context))) { - mOnlyDictionarySetForDebug = new HashSet<String>(); - mOnlyDictionarySetForDebug.add(Dictionary.TYPE_PERSONALIZATION); - mOnlyDictionarySetForDebug.add(Dictionary.TYPE_PERSONALIZATION_PREDICTION_IN_JAVA); - } - } - - @UsedForTesting - Suggest(final AssetFileAddress[] dictionaryList, final Locale locale) { - final Dictionary mainDict = DictionaryFactory.createDictionaryForTest(dictionaryList, - false /* useFullEditDistance */, locale); - mLocale = locale; - mMainDictionary = mainDict; - addOrReplaceDictionaryInternal(Dictionary.TYPE_MAIN, mainDict); - } - - private void initAsynchronously(final Context context, final Locale locale, - final SuggestInitializationListener listener) { - resetMainDict(context, locale, listener); - } - - private void addOrReplaceDictionaryInternal(final String key, final Dictionary dict) { - if (mOnlyDictionarySetForDebug != null && !mOnlyDictionarySetForDebug.contains(key)) { - Log.w(TAG, "Ignore add " + key + " dictionary for debug."); - return; - } - addOrReplaceDictionary(mDictionaries, key, dict); - } - - private static void addOrReplaceDictionary( - final ConcurrentHashMap<String, Dictionary> dictionaries, - final String key, final Dictionary dict) { - final Dictionary oldDict = (dict == null) - ? dictionaries.remove(key) - : dictionaries.put(key, dict); - if (oldDict != null && dict != oldDict) { - oldDict.close(); - } + public Suggest(final DictionaryFacilitator dictionaryFacilitator) { + mDictionaryFacilitator = dictionaryFacilitator; } - public void resetMainDict(final Context context, final Locale locale, - final SuggestInitializationListener listener) { - mIsCurrentlyWaitingForMainDictionary = true; - mMainDictionary = null; - if (listener != null) { - listener.onUpdateMainDictionaryAvailability(hasMainDictionary()); - } - new Thread("InitializeBinaryDictionary") { - @Override - public void run() { - final DictionaryCollection newMainDict = - DictionaryFactory.createMainDictionaryFromManager(context, locale); - addOrReplaceDictionaryInternal(Dictionary.TYPE_MAIN, newMainDict); - mMainDictionary = newMainDict; - if (listener != null) { - listener.onUpdateMainDictionaryAvailability(hasMainDictionary()); - } - mIsCurrentlyWaitingForMainDictionary = false; - } - }.start(); + public Locale getLocale() { + return mDictionaryFacilitator.getLocale(); } - // The main dictionary could have been loaded asynchronously. Don't cache the return value - // of this method. - public boolean hasMainDictionary() { - return null != mMainDictionary && mMainDictionary.isInitialized(); - } - - @UsedForTesting - public boolean isCurrentlyWaitingForMainDictionary() { - return mIsCurrentlyWaitingForMainDictionary; - } - - public Dictionary getMainDictionary() { - return mMainDictionary; - } - - public ContactsBinaryDictionary getContactsDictionary() { - return mContactsDict; - } - - public ConcurrentHashMap<String, Dictionary> getUnigramDictionaries() { - return mDictionaries; - } - - /** - * Sets an optional user dictionary resource to be loaded. The user dictionary is consulted - * before the main dictionary, if set. This refers to the system-managed user dictionary. - */ - public void setUserDictionary(final UserBinaryDictionary userDictionary) { - addOrReplaceDictionaryInternal(Dictionary.TYPE_USER, userDictionary); - } - - /** - * Sets an optional contacts dictionary resource to be loaded. It is also possible to remove - * the contacts dictionary by passing null to this method. In this case no contacts dictionary - * won't be used. - */ - public void setContactsDictionary(final ContactsBinaryDictionary contactsDictionary) { - mContactsDict = contactsDictionary; - addOrReplaceDictionaryInternal(Dictionary.TYPE_CONTACTS, contactsDictionary); - } - - public void setUserHistoryDictionary(final UserHistoryDictionary userHistoryDictionary) { - addOrReplaceDictionaryInternal(Dictionary.TYPE_USER_HISTORY, userHistoryDictionary); - } - - public void setPersonalizationPredictionDictionary( - final PersonalizationPredictionDictionary personalizationPredictionDictionary) { - addOrReplaceDictionaryInternal(Dictionary.TYPE_PERSONALIZATION_PREDICTION_IN_JAVA, - personalizationPredictionDictionary); - } - - public void setPersonalizationDictionary( - final PersonalizationDictionary personalizationDictionary) { - addOrReplaceDictionaryInternal(Dictionary.TYPE_PERSONALIZATION, - personalizationDictionary); - } - - public void setAutoCorrectionThreshold(float threshold) { + public void setAutoCorrectionThreshold(final float threshold) { mAutoCorrectionThreshold = threshold; } @@ -215,71 +72,84 @@ public final class Suggest { } public void getSuggestedWords(final WordComposer wordComposer, - final String prevWordForBigram, final ProximityInfo proximityInfo, + final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, final boolean blockOffensiveWords, final boolean isCorrectionEnabled, final int[] additionalFeaturesOptions, final int sessionId, final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { - LatinImeLogger.onStartSuggestion(prevWordForBigram); if (wordComposer.isBatchMode()) { - getSuggestedWordsForBatchInput(wordComposer, prevWordForBigram, proximityInfo, + getSuggestedWordsForBatchInput(wordComposer, prevWordsInfo, proximityInfo, blockOffensiveWords, additionalFeaturesOptions, sessionId, sequenceNumber, callback); } else { - getSuggestedWordsForTypingInput(wordComposer, prevWordForBigram, proximityInfo, + getSuggestedWordsForTypingInput(wordComposer, prevWordsInfo, proximityInfo, blockOffensiveWords, isCorrectionEnabled, additionalFeaturesOptions, sequenceNumber, callback); } } + private static ArrayList<SuggestedWordInfo> getTransformedSuggestedWordInfoList( + final WordComposer wordComposer, final SuggestionResults results, + final int trailingSingleQuotesCount) { + final boolean shouldMakeSuggestionsAllUpperCase = wordComposer.isAllUpperCase() + && !wordComposer.isResumed(); + final boolean isOnlyFirstCharCapitalized = + wordComposer.isOrWillBeOnlyFirstCharCapitalized(); + + final ArrayList<SuggestedWordInfo> suggestionsContainer = new ArrayList<>(results); + final int suggestionsCount = suggestionsContainer.size(); + if (isOnlyFirstCharCapitalized || shouldMakeSuggestionsAllUpperCase + || 0 != trailingSingleQuotesCount) { + for (int i = 0; i < suggestionsCount; ++i) { + final SuggestedWordInfo wordInfo = suggestionsContainer.get(i); + final SuggestedWordInfo transformedWordInfo = getTransformedSuggestedWordInfo( + wordInfo, results.mLocale, shouldMakeSuggestionsAllUpperCase, + isOnlyFirstCharCapitalized, trailingSingleQuotesCount); + suggestionsContainer.set(i, transformedWordInfo); + } + } + return suggestionsContainer; + } + + private static String getWhitelistedWordOrNull(final ArrayList<SuggestedWordInfo> suggestions) { + if (suggestions.isEmpty()) { + return null; + } + final SuggestedWordInfo firstSuggestedWordInfo = suggestions.get(0); + if (!firstSuggestedWordInfo.isKindOf(SuggestedWordInfo.KIND_WHITELIST)) { + return null; + } + return firstSuggestedWordInfo.mWord; + } + // Retrieves suggestions for the typing input // and calls the callback function with the suggestions. private void getSuggestedWordsForTypingInput(final WordComposer wordComposer, - final String prevWordForBigram, final ProximityInfo proximityInfo, + final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, final boolean blockOffensiveWords, final boolean isCorrectionEnabled, final int[] additionalFeaturesOptions, final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { - final int trailingSingleQuotesCount = wordComposer.trailingSingleQuotesCount(); - final BoundedTreeSet suggestionsSet = new BoundedTreeSet(sSuggestedWordInfoComparator, - MAX_SUGGESTIONS); - final String typedWord = wordComposer.getTypedWord(); + final int trailingSingleQuotesCount = StringUtils.getTrailingSingleQuotesCount(typedWord); final String consideredWord = trailingSingleQuotesCount > 0 ? typedWord.substring(0, typedWord.length() - trailingSingleQuotesCount) : typedWord; - LatinImeLogger.onAddSuggestedWord(typedWord, Dictionary.TYPE_USER_TYPED); - final WordComposer wordComposerForLookup; - if (trailingSingleQuotesCount > 0) { - wordComposerForLookup = new WordComposer(wordComposer); - for (int i = trailingSingleQuotesCount - 1; i >= 0; --i) { - wordComposerForLookup.deleteLast(); - } - } else { - wordComposerForLookup = wordComposer; - } - - for (final String key : mDictionaries.keySet()) { - final Dictionary dictionary = mDictionaries.get(key); - suggestionsSet.addAll(dictionary.getSuggestions(wordComposerForLookup, - prevWordForBigram, proximityInfo, blockOffensiveWords, - additionalFeaturesOptions)); - } + final SuggestionResults suggestionResults = mDictionaryFacilitator.getSuggestionResults( + wordComposer, prevWordsInfo, proximityInfo, blockOffensiveWords, + additionalFeaturesOptions, SESSION_TYPING); + final ArrayList<SuggestedWordInfo> suggestionsContainer = + getTransformedSuggestedWordInfoList(wordComposer, suggestionResults, + trailingSingleQuotesCount); + final boolean didRemoveTypedWord = + SuggestedWordInfo.removeDups(wordComposer.getTypedWord(), suggestionsContainer); - final String whitelistedWord; - if (suggestionsSet.isEmpty()) { - whitelistedWord = null; - } else if (SuggestedWordInfo.KIND_WHITELIST != suggestionsSet.first().mKind) { - whitelistedWord = null; - } else { - whitelistedWord = suggestionsSet.first().mWord; - } + final String whitelistedWord = getWhitelistedWordOrNull(suggestionsContainer); + final boolean resultsArePredictions = !wordComposer.isComposingWord(); - // 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 && !AutoCorrectionUtils.isValidWord(this, - consideredWord, wordComposer.isFirstCharCapitalized())); + // We allow auto-correction if we have a whitelisted word, or if the word had more than + // one char and was not suggested. + final boolean allowsToBeAutoCorrected = (null != whitelistedWord) + || (consideredWord.length() > 1 && !didRemoveTypedWord); final boolean hasAutoCorrection; // TODO: using isCorrectionEnabled here is not very good. It's probably useless, because @@ -287,10 +157,11 @@ public final class Suggest { // same time, it feels wrong that the SuggestedWord object includes information about // the current settings. It may also be useful to know, when the setting is off, whether // the word *would* have been auto-corrected. - if (!isCorrectionEnabled || !allowsToBeAutoCorrected || !wordComposer.isComposingWord() - || suggestionsSet.isEmpty() || wordComposer.hasDigits() - || wordComposer.isMostlyCaps() || wordComposer.isResumed() || !hasMainDictionary() - || SuggestedWordInfo.KIND_SHORTCUT == suggestionsSet.first().mKind) { + if (!isCorrectionEnabled || !allowsToBeAutoCorrected || resultsArePredictions + || suggestionResults.isEmpty() || wordComposer.hasDigits() + || wordComposer.isMostlyCaps() || wordComposer.isResumed() + || !mDictionaryFacilitator.hasInitializedMainDictionary() + || suggestionResults.first().isKindOf(SuggestedWordInfo.KIND_SHORTCUT)) { // If we don't have a main dictionary, we never want to auto-correct. The reason for // this is, the user may have a contact whose name happens to match a valid word in // their language, and it will unexpectedly auto-correct. For example, if the user @@ -302,28 +173,7 @@ public final class Suggest { hasAutoCorrection = false; } else { hasAutoCorrection = AutoCorrectionUtils.suggestionExceedsAutoCorrectionThreshold( - suggestionsSet.first(), consideredWord, mAutoCorrectionThreshold); - } - - 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); - } - } - - for (int i = 0; i < suggestionsCount; ++i) { - final SuggestedWordInfo wordInfo = suggestionsContainer.get(i); - LatinImeLogger.onAddSuggestedWord(wordInfo.mWord.toString(), - wordInfo.mSourceDict.mDictType); + suggestionResults.first(), consideredWord, mAutoCorrectionThreshold); } if (!TextUtils.isEmpty(typedWord)) { @@ -333,7 +183,6 @@ public final class Suggest { SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); } - SuggestedWordInfo.removeDups(suggestionsContainer); final ArrayList<SuggestedWordInfo> suggestionsList; if (DBG && !suggestionsContainer.isEmpty()) { @@ -343,40 +192,27 @@ public final class Suggest { } callback.onGetSuggestedWords(new SuggestedWords(suggestionsList, + suggestionResults.mRawSuggestions, // 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 */, - hasAutoCorrection, /* willAutoCorrect */ - false /* isPunctuationSuggestions */, - false /* isObsoleteSuggestions */, - !wordComposer.isComposingWord() /* isPrediction */, sequenceNumber)); + !resultsArePredictions && !allowsToBeAutoCorrected /* typedWordValid */, + hasAutoCorrection /* willAutoCorrect */, + false /* isObsoleteSuggestions */, resultsArePredictions, sequenceNumber)); } // Retrieves suggestions for the batch input // and calls the callback function with the suggestions. private void getSuggestedWordsForBatchInput(final WordComposer wordComposer, - final String prevWordForBigram, final ProximityInfo proximityInfo, + final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, final int sessionId, final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { - final BoundedTreeSet suggestionsSet = new BoundedTreeSet(sSuggestedWordInfoComparator, - MAX_SUGGESTIONS); - - // At second character typed, search the unigrams (scores being affected by bigrams) - for (final String key : mDictionaries.keySet()) { - final Dictionary dictionary = mDictionaries.get(key); - suggestionsSet.addAll(dictionary.getSuggestionsWithSessionId(wordComposer, - prevWordForBigram, proximityInfo, blockOffensiveWords, - additionalFeaturesOptions, sessionId)); - } - - for (SuggestedWordInfo wordInfo : suggestionsSet) { - LatinImeLogger.onAddSuggestedWord(wordInfo.mWord, wordInfo.mSourceDict.mDictType); - } - + final SuggestionResults suggestionResults = mDictionaryFacilitator.getSuggestionResults( + wordComposer, prevWordsInfo, proximityInfo, blockOffensiveWords, + additionalFeaturesOptions, sessionId); final ArrayList<SuggestedWordInfo> suggestionsContainer = - CollectionUtils.newArrayList(suggestionsSet); + new ArrayList<>(suggestionResults); final int suggestionsCount = suggestionsContainer.size(); final boolean isFirstCharCapitalized = wordComposer.wasShiftedNoLock(); final boolean isAllUpperCase = wordComposer.isAllUpperCase(); @@ -384,7 +220,7 @@ public final class Suggest { for (int i = 0; i < suggestionsCount; ++i) { final SuggestedWordInfo wordInfo = suggestionsContainer.get(i); final SuggestedWordInfo transformedWordInfo = getTransformedSuggestedWordInfo( - wordInfo, mLocale, isAllUpperCase, isFirstCharCapitalized, + wordInfo, suggestionResults.mLocale, isAllUpperCase, isFirstCharCapitalized, 0 /* trailingSingleQuotesCount */); suggestionsContainer.set(i, transformedWordInfo); } @@ -395,7 +231,7 @@ public final class Suggest { final SuggestedWordInfo rejected = suggestionsContainer.remove(0); suggestionsContainer.add(1, rejected); } - SuggestedWordInfo.removeDups(suggestionsContainer); + SuggestedWordInfo.removeDups(null /* typedWord */, suggestionsContainer); // For some reason some suggestions with MIN_VALUE are making their way here. // TODO: Find a more robust way to detect distractors. @@ -408,9 +244,9 @@ public final class Suggest { // In the batch input mode, the most relevant suggested word should act as a "typed word" // (typedWordValid=true), not as an "auto correct word" (willAutoCorrect=false). callback.onGetSuggestedWords(new SuggestedWords(suggestionsContainer, + suggestionResults.mRawSuggestions, true /* typedWordValid */, false /* willAutoCorrect */, - false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */, false /* isPrediction */, sequenceNumber)); } @@ -420,19 +256,19 @@ public final class Suggest { final SuggestedWordInfo typedWordInfo = suggestions.get(0); typedWordInfo.setDebugString("+"); final int suggestionsSize = suggestions.size(); - final ArrayList<SuggestedWordInfo> suggestionsList = - CollectionUtils.newArrayList(suggestionsSize); + final ArrayList<SuggestedWordInfo> suggestionsList = new ArrayList<>(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. for (int i = 0; i < suggestionsSize - 1; ++i) { final SuggestedWordInfo cur = suggestions.get(i + 1); - final float normalizedScore = BinaryDictionary.calcNormalizedScore( + final float normalizedScore = BinaryDictionaryUtils.calcNormalizedScore( typedWord, cur.toString(), cur.mScore); final String scoreInfoString; if (normalizedScore > 0) { scoreInfoString = String.format( - Locale.ROOT, "%d (%4.2f)", cur.mScore, normalizedScore); + Locale.ROOT, "%d (%4.2f), %s", cur.mScore, normalizedScore, + cur.mSourceDict.mDictType); } else { scoreInfoString = Integer.toString(cur.mScore); } @@ -442,29 +278,13 @@ public final class Suggest { return suggestionsList; } - private static final class SuggestedWordInfoComparator - implements Comparator<SuggestedWordInfo> { - // This comparator ranks the word info with the higher frequency first. That's because - // that's the order we want our elements in. - @Override - public int compare(final SuggestedWordInfo o1, final SuggestedWordInfo o2) { - if (o1.mScore > o2.mScore) return -1; - if (o1.mScore < o2.mScore) return 1; - if (o1.mCodePointCount < o2.mCodePointCount) return -1; - if (o1.mCodePointCount > o2.mCodePointCount) return 1; - return o1.mWord.compareTo(o2.mWord); - } - } - private static final SuggestedWordInfoComparator sSuggestedWordInfoComparator = - new SuggestedWordInfoComparator(); - /* package for test */ static SuggestedWordInfo getTransformedSuggestedWordInfo( final SuggestedWordInfo wordInfo, final Locale locale, final boolean isAllUpperCase, - final boolean isFirstCharCapitalized, final int trailingSingleQuotesCount) { + final boolean isOnlyFirstCharCapitalized, final int trailingSingleQuotesCount) { final StringBuilder sb = new StringBuilder(wordInfo.mWord.length()); if (isAllUpperCase) { sb.append(wordInfo.mWord.toUpperCase(locale)); - } else if (isFirstCharCapitalized) { + } else if (isOnlyFirstCharCapitalized) { sb.append(StringUtils.capitalizeFirstCodePoint(wordInfo.mWord, locale)); } else { sb.append(wordInfo.mWord); @@ -477,17 +297,8 @@ public final class Suggest { for (int i = quotesToAppend - 1; i >= 0; --i) { sb.appendCodePoint(Constants.CODE_SINGLE_QUOTE); } - return new SuggestedWordInfo(sb.toString(), wordInfo.mScore, wordInfo.mKind, + return new SuggestedWordInfo(sb.toString(), wordInfo.mScore, wordInfo.mKindAndFlags, wordInfo.mSourceDict, wordInfo.mIndexOfTouchPointOfSecondWord, wordInfo.mAutoCommitFirstWordConfidence); } - - public void close() { - final HashSet<Dictionary> dictionaries = CollectionUtils.newHashSet(); - dictionaries.addAll(mDictionaries.values()); - for (final Dictionary dictionary : dictionaries) { - dictionary.close(); - } - mMainDictionary = null; - } } diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java index 97c89dd4e..e587b18c9 100644 --- a/java/src/com/android/inputmethod/latin/SuggestedWords.java +++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java @@ -19,58 +19,76 @@ package com.android.inputmethod.latin; import android.text.TextUtils; import android.view.inputmethod.CompletionInfo; -import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.StringUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; -public final class SuggestedWords { +public class SuggestedWords { public static final int INDEX_OF_TYPED_WORD = 0; public static final int INDEX_OF_AUTO_CORRECTION = 1; public static final int NOT_A_SEQUENCE_NUMBER = -1; - private static final ArrayList<SuggestedWordInfo> EMPTY_WORD_INFO_LIST = - CollectionUtils.newArrayList(0); + // The maximum number of suggestions available. + public static final int MAX_SUGGESTIONS = 18; + + private static final ArrayList<SuggestedWordInfo> EMPTY_WORD_INFO_LIST = new ArrayList<>(0); public static final SuggestedWords EMPTY = new SuggestedWords( - EMPTY_WORD_INFO_LIST, false, false, false, false, false); + EMPTY_WORD_INFO_LIST, null /* rawSuggestions */, false, false, false, false); + public final String mTypedWord; public final boolean mTypedWordValid; // Note: this INCLUDES cases where the word will auto-correct to itself. A good definition // of what this flag means would be "the top suggestion is strong enough to auto-correct", // whether this exactly matches the user entry or not. public final boolean mWillAutoCorrect; - public final boolean mIsPunctuationSuggestions; public final boolean mIsObsoleteSuggestions; public final boolean mIsPrediction; public final int mSequenceNumber; // Sequence number for auto-commit. - private final ArrayList<SuggestedWordInfo> mSuggestedWordInfoList; + protected final ArrayList<SuggestedWordInfo> mSuggestedWordInfoList; + public final ArrayList<SuggestedWordInfo> mRawSuggestions; public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList, + final ArrayList<SuggestedWordInfo> rawSuggestions, final boolean typedWordValid, final boolean willAutoCorrect, - final boolean isPunctuationSuggestions, final boolean isObsoleteSuggestions, final boolean isPrediction) { - this(suggestedWordInfoList, typedWordValid, willAutoCorrect, isPunctuationSuggestions, + this(suggestedWordInfoList, rawSuggestions, typedWordValid, willAutoCorrect, isObsoleteSuggestions, isPrediction, NOT_A_SEQUENCE_NUMBER); } public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList, + final ArrayList<SuggestedWordInfo> rawSuggestions, + final boolean typedWordValid, + final boolean willAutoCorrect, + final boolean isObsoleteSuggestions, + final boolean isPrediction, + final int sequenceNumber) { + this(suggestedWordInfoList, rawSuggestions, + (suggestedWordInfoList.isEmpty() || isPrediction) ? null + : suggestedWordInfoList.get(INDEX_OF_TYPED_WORD).mWord, + typedWordValid, willAutoCorrect, isObsoleteSuggestions, isPrediction, + sequenceNumber); + } + + public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList, + final ArrayList<SuggestedWordInfo> rawSuggestions, + final String typedWord, final boolean typedWordValid, final boolean willAutoCorrect, - final boolean isPunctuationSuggestions, final boolean isObsoleteSuggestions, final boolean isPrediction, final int sequenceNumber) { mSuggestedWordInfoList = suggestedWordInfoList; + mRawSuggestions = rawSuggestions; mTypedWordValid = typedWordValid; mWillAutoCorrect = willAutoCorrect; - mIsPunctuationSuggestions = isPunctuationSuggestions; mIsObsoleteSuggestions = isObsoleteSuggestions; mIsPrediction = isPrediction; mSequenceNumber = sequenceNumber; + mTypedWord = typedWord; } public boolean isEmpty() { @@ -81,10 +99,32 @@ public final class SuggestedWords { return mSuggestedWordInfoList.size(); } + /** + * Get suggested word at <code>index</code>. + * @param index The index of the suggested word. + * @return The suggested word. + */ public String getWord(final int index) { return mSuggestedWordInfoList.get(index).mWord; } + /** + * Get displayed text at <code>index</code>. + * In RTL languages, the displayed text on the suggestion strip may be different from the + * suggested word that is returned from {@link #getWord(int)}. For example the displayed text + * of punctuation suggestion "(" should be ")". + * @param index The index of the text to display. + * @return The text to be displayed. + */ + public String getLabel(final int index) { + return mSuggestedWordInfoList.get(index).mWord; + } + + /** + * Get {@link SuggestedWordInfo} object at <code>index</code>. + * @param index The index of the {@link SuggestedWordInfo}. + * @return The {@link SuggestedWordInfo} object. + */ public SuggestedWordInfo getInfo(final int index) { return mSuggestedWordInfoList.get(index); } @@ -104,8 +144,12 @@ public final class SuggestedWords { return debugString; } - public boolean willAutoCorrect() { - return mWillAutoCorrect; + /** + * The predicator to tell whether this object represents punctuation suggestions. + * @return false if this object desn't represent punctuation suggestions. + */ + public boolean isPunctuationSuggestions() { + return false; } @Override @@ -114,23 +158,17 @@ public final class SuggestedWords { return "SuggestedWords:" + " mTypedWordValid=" + mTypedWordValid + " mWillAutoCorrect=" + mWillAutoCorrect - + " mIsPunctuationSuggestions=" + mIsPunctuationSuggestions + " words=" + Arrays.toString(mSuggestedWordInfoList.toArray()); } public static ArrayList<SuggestedWordInfo> getFromApplicationSpecifiedCompletions( final CompletionInfo[] infos) { - final ArrayList<SuggestedWordInfo> result = CollectionUtils.newArrayList(); + final ArrayList<SuggestedWordInfo> result = new ArrayList<>(); for (final CompletionInfo info : infos) { - if (info == null) continue; - final CharSequence text = info.getText(); - if (null == text) continue; - final SuggestedWordInfo suggestedWordInfo = new SuggestedWordInfo(text.toString(), - SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_APP_DEFINED, - Dictionary.DICTIONARY_APPLICATION_DEFINED, - SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, - SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */); - result.add(suggestedWordInfo); + if (null == info || null == info.getText()) { + continue; + } + result.add(new SuggestedWordInfo(info)); } return result; } @@ -139,8 +177,8 @@ public final class SuggestedWords { // and replace it with what the user currently typed. public static ArrayList<SuggestedWordInfo> getTypedWordAndPreviousSuggestions( final String typedWord, final SuggestedWords previousSuggestions) { - final ArrayList<SuggestedWordInfo> suggestionsList = CollectionUtils.newArrayList(); - final HashSet<String> alreadySeen = CollectionUtils.newHashSet(); + final ArrayList<SuggestedWordInfo> suggestionsList = new ArrayList<>(); + final HashSet<String> alreadySeen = new HashSet<>(); suggestionsList.add(new SuggestedWordInfo(typedWord, SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED, SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, @@ -150,7 +188,7 @@ public final class SuggestedWords { for (int index = 1; index < previousSize; index++) { final SuggestedWordInfo prevWordInfo = previousSuggestions.getInfo(index); final String prevWord = prevWordInfo.mWord; - // Filter out duplicate suggestion. + // Filter out duplicate suggestions. if (!alreadySeen.contains(prevWord)) { suggestionsList.add(prevWordInfo); alreadySeen.add(prevWord); @@ -169,7 +207,8 @@ public final class SuggestedWords { public static final int NOT_AN_INDEX = -1; public static final int NOT_A_CONFIDENCE = -1; public static final int MAX_SCORE = Integer.MAX_VALUE; - public static final int KIND_MASK_KIND = 0xFF; // Mask to get only the kind + + private static final int KIND_MASK_KIND = 0xFF; // Mask to get only the kind 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) @@ -184,13 +223,16 @@ public final class SuggestedWords { public static final int KIND_RESUMED = 9; public static final int KIND_OOV_CORRECTION = 10; // Most probable string correction - public static final int KIND_MASK_FLAGS = 0xFFFFFF00; // Mask to get the flags public static final int KIND_FLAG_POSSIBLY_OFFENSIVE = 0x80000000; public static final int KIND_FLAG_EXACT_MATCH = 0x40000000; + public static final int KIND_FLAG_EXACT_MATCH_WITH_INTENTIONAL_OMISSION = 0x20000000; public final String mWord; + // The completion info from the application. Null for suggestions that don't come from + // the application (including keyboard-computed ones, so this is almost always null) + public final CompletionInfo mApplicationSpecifiedCompletionInfo; public final int mScore; - public final int mKind; // one of the KIND_* constants above + public final int mKindAndFlags; public final int mCodePointCount; public final Dictionary mSourceDict; // For auto-commit. This keeps track of the index inside the touch coordinates array @@ -206,25 +248,63 @@ public final class SuggestedWords { * Create a new suggested word info. * @param word The string to suggest. * @param score A measure of how likely this suggestion is. - * @param kind The kind of suggestion, as one of the above KIND_* constants. + * @param kindAndFlags The kind of suggestion, as one of the above KIND_* constants with + * flags. * @param sourceDict What instance of Dictionary produced this suggestion. * @param indexOfTouchPointOfSecondWord See mIndexOfTouchPointOfSecondWord. * @param autoCommitFirstWordConfidence See mAutoCommitFirstWordConfidence. */ - public SuggestedWordInfo(final String word, final int score, final int kind, + public SuggestedWordInfo(final String word, final int score, final int kindAndFlags, final Dictionary sourceDict, final int indexOfTouchPointOfSecondWord, final int autoCommitFirstWordConfidence) { mWord = word; + mApplicationSpecifiedCompletionInfo = null; mScore = score; - mKind = kind; + mKindAndFlags = kindAndFlags; mSourceDict = sourceDict; mCodePointCount = StringUtils.codePointCount(mWord); mIndexOfTouchPointOfSecondWord = indexOfTouchPointOfSecondWord; mAutoCommitFirstWordConfidence = autoCommitFirstWordConfidence; } + /** + * Create a new suggested word info from an application-specified completion. + * If the passed argument or its contained text is null, this throws a NPE. + * @param applicationSpecifiedCompletion The application-specified completion info. + */ + public SuggestedWordInfo(final CompletionInfo applicationSpecifiedCompletion) { + mWord = applicationSpecifiedCompletion.getText().toString(); + mApplicationSpecifiedCompletionInfo = applicationSpecifiedCompletion; + mScore = SuggestedWordInfo.MAX_SCORE; + mKindAndFlags = SuggestedWordInfo.KIND_APP_DEFINED; + mSourceDict = Dictionary.DICTIONARY_APPLICATION_DEFINED; + mCodePointCount = StringUtils.codePointCount(mWord); + mIndexOfTouchPointOfSecondWord = SuggestedWordInfo.NOT_AN_INDEX; + mAutoCommitFirstWordConfidence = SuggestedWordInfo.NOT_A_CONFIDENCE; + } + public boolean isEligibleForAutoCommit() { - return (KIND_CORRECTION == mKind && NOT_AN_INDEX != mIndexOfTouchPointOfSecondWord); + return (isKindOf(KIND_CORRECTION) && NOT_AN_INDEX != mIndexOfTouchPointOfSecondWord); + } + + public int getKind() { + return (mKindAndFlags & KIND_MASK_KIND); + } + + public boolean isKindOf(final int kind) { + return getKind() == kind; + } + + public boolean isPossiblyOffensive() { + return (mKindAndFlags & KIND_FLAG_POSSIBLY_OFFENSIVE) != 0; + } + + public boolean isExactMatch() { + return (mKindAndFlags & KIND_FLAG_EXACT_MATCH) != 0; + } + + public boolean isExactMatchWithIntentionalOmission() { + return (mKindAndFlags & KIND_FLAG_EXACT_MATCH_WITH_INTENTIONAL_OMISSION) != 0; } public void setDebugString(final String str) { @@ -236,10 +316,6 @@ public final class SuggestedWords { return mDebugString; } - public int codePointCount() { - return mCodePointCount; - } - public int codePointAt(int i) { return mWord.codePointAt(i); } @@ -253,42 +329,60 @@ public final class SuggestedWords { } } - // TODO: Consolidate this method and StringUtils.removeDupes() in the future. - public static void removeDups(ArrayList<SuggestedWordInfo> candidates) { - if (candidates.size() <= 1) { - return; + // This will always remove the higher index if a duplicate is found. + public static boolean removeDups(final String typedWord, + ArrayList<SuggestedWordInfo> candidates) { + if (candidates.isEmpty()) { + return false; + } + final boolean didRemoveTypedWord; + if (!TextUtils.isEmpty(typedWord)) { + didRemoveTypedWord = removeSuggestedWordInfoFrom(typedWord, candidates, + -1 /* startIndexExclusive */); + } else { + didRemoveTypedWord = false; } - int i = 1; - while (i < candidates.size()) { - final SuggestedWordInfo cur = candidates.get(i); - for (int j = 0; j < i; ++j) { - final SuggestedWordInfo previous = candidates.get(j); - if (cur.mWord.equals(previous.mWord)) { - candidates.remove(cur.mScore < previous.mScore ? i : j); - --i; - break; - } + for (int i = 0; i < candidates.size(); ++i) { + removeSuggestedWordInfoFrom(candidates.get(i).mWord, candidates, + i /* startIndexExclusive */); + } + return didRemoveTypedWord; + } + + private static boolean removeSuggestedWordInfoFrom(final String word, + final ArrayList<SuggestedWordInfo> candidates, final int startIndexExclusive) { + boolean didRemove = false; + for (int i = startIndexExclusive + 1; i < candidates.size(); ++i) { + final SuggestedWordInfo previous = candidates.get(i); + if (word.equals(previous.mWord)) { + didRemove = true; + candidates.remove(i); + --i; } - ++i; } + return didRemove; } } // SuggestedWords is an immutable object, as much as possible. We must not just remove // words from the member ArrayList as some other parties may expect the object to never change. public SuggestedWords getSuggestedWordsExcludingTypedWord() { - final ArrayList<SuggestedWordInfo> newSuggestions = CollectionUtils.newArrayList(); + final ArrayList<SuggestedWordInfo> newSuggestions = new ArrayList<>(); + String typedWord = null; for (int i = 0; i < mSuggestedWordInfoList.size(); ++i) { final SuggestedWordInfo info = mSuggestedWordInfoList.get(i); - if (SuggestedWordInfo.KIND_TYPED != info.mKind) { + if (!info.isKindOf(SuggestedWordInfo.KIND_TYPED)) { newSuggestions.add(info); + } else { + assert(null == typedWord); + typedWord = info.mWord; } } // We should never autocorrect, so we say the typed word is valid. Also, in this case, // no auto-correction should take place hence willAutoCorrect = false. - return new SuggestedWords(newSuggestions, true /* typedWordValid */, - false /* willAutoCorrect */, mIsPunctuationSuggestions, mIsObsoleteSuggestions, - mIsPrediction); + return new SuggestedWords(newSuggestions, null /* rawSuggestions */, typedWord, + true /* typedWordValid */, false /* willAutoCorrect */, mIsObsoleteSuggestions, + mIsPrediction, NOT_A_SEQUENCE_NUMBER); } // Creates a new SuggestedWordInfo from the currently suggested words that removes all but the @@ -297,17 +391,16 @@ public final class SuggestedWords { // we should only suggest replacements for this last word. // TODO: make this work with languages without spaces. public SuggestedWords getSuggestedWordsForLastWordOfPhraseGesture() { - final ArrayList<SuggestedWordInfo> newSuggestions = CollectionUtils.newArrayList(); + final ArrayList<SuggestedWordInfo> newSuggestions = new ArrayList<>(); for (int i = 0; i < mSuggestedWordInfoList.size(); ++i) { final SuggestedWordInfo info = mSuggestedWordInfoList.get(i); final int indexOfLastSpace = info.mWord.lastIndexOf(Constants.CODE_SPACE) + 1; final String lastWord = info.mWord.substring(indexOfLastSpace); - newSuggestions.add(new SuggestedWordInfo(lastWord, info.mScore, info.mKind, + newSuggestions.add(new SuggestedWordInfo(lastWord, info.mScore, info.mKindAndFlags, info.mSourceDict, SuggestedWordInfo.NOT_AN_INDEX, SuggestedWordInfo.NOT_A_CONFIDENCE)); } - return new SuggestedWords(newSuggestions, mTypedWordValid, - mWillAutoCorrect, mIsPunctuationSuggestions, mIsObsoleteSuggestions, - mIsPrediction); + return new SuggestedWords(newSuggestions, null /* rawSuggestions */, mTypedWordValid, + mWillAutoCorrect, mIsObsoleteSuggestions, mIsPrediction); } } diff --git a/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java b/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java new file mode 100644 index 000000000..e4ee42660 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.Process; +import android.preference.PreferenceManager; +import android.util.Log; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.InputMethodSubtype; + +import com.android.inputmethod.compat.IntentCompatUtils; +import com.android.inputmethod.latin.settings.Settings; +import com.android.inputmethod.latin.setup.LauncherIconVisibilityManager; +import com.android.inputmethod.latin.setup.SetupActivity; +import com.android.inputmethod.latin.utils.UncachedInputMethodManagerUtils; + +/** + * This class detects the {@link Intent#ACTION_MY_PACKAGE_REPLACED} broadcast intent when this IME + * package has been replaced by a newer version of the same package. This class also detects + * {@link Intent#ACTION_BOOT_COMPLETED} and {@link Intent#ACTION_USER_INITIALIZE} broadcast intent. + * + * If this IME has already been installed in the system image and a new version of this IME has + * been installed, {@link Intent#ACTION_MY_PACKAGE_REPLACED} is received by this receiver and it + * will hide the setup wizard's icon. + * + * If this IME has already been installed in the data partition and a new version of this IME has + * been installed, {@link Intent#ACTION_MY_PACKAGE_REPLACED} is received by this receiver but it + * will not hide the setup wizard's icon, and the icon will appear on the launcher. + * + * If this IME hasn't been installed yet and has been newly installed, no + * {@link Intent#ACTION_MY_PACKAGE_REPLACED} will be sent and the setup wizard's icon will appear + * on the launcher. + * + * When the device has been booted, {@link Intent#ACTION_BOOT_COMPLETED} is received by this + * receiver and it checks whether the setup wizard's icon should be appeared or not on the launcher + * depending on which partition this IME is installed. + * + * When a multiuser account has been created, {@link Intent#ACTION_USER_INITIALIZE} is received + * by this receiver and it checks the whether the setup wizard's icon should be appeared or not on + * the launcher depending on which partition this IME is installed. + */ +public final class SystemBroadcastReceiver extends BroadcastReceiver { + private static final String TAG = SystemBroadcastReceiver.class.getSimpleName(); + + @Override + public void onReceive(final Context context, final Intent intent) { + final String intentAction = intent.getAction(); + if (Intent.ACTION_MY_PACKAGE_REPLACED.equals(intentAction)) { + Log.i(TAG, "Package has been replaced: " + context.getPackageName()); + } else if (Intent.ACTION_BOOT_COMPLETED.equals(intentAction)) { + Log.i(TAG, "Boot has been completed"); + } else if (IntentCompatUtils.is_ACTION_USER_INITIALIZE(intentAction)) { + Log.i(TAG, "User initialize"); + } + + LauncherIconVisibilityManager.onReceiveGlobalIntent(intentAction, context); + + if (Intent.ACTION_MY_PACKAGE_REPLACED.equals(intentAction)) { + // Need to restore additional subtypes because system always clears additional + // subtypes when the package is replaced. + RichInputMethodManager.init(context); + final RichInputMethodManager richImm = RichInputMethodManager.getInstance(); + final InputMethodSubtype[] additionalSubtypes = richImm.getAdditionalSubtypes(context); + richImm.setAdditionalInputMethodSubtypes(additionalSubtypes); + } + + // The process that hosts this broadcast receiver is invoked and remains alive even after + // 1) the package has been re-installed, 2) the device has just booted, + // 3) a new user has been created. + // There is no good reason to keep the process alive if this IME isn't a current IME. + final InputMethodManager imm = + (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE); + // Called to check whether this IME has been triggered by the current user or not + final boolean isInputMethodManagerValidForUserOfThisProcess = + !imm.getInputMethodList().isEmpty(); + final boolean isCurrentImeOfCurrentUser = isInputMethodManagerValidForUserOfThisProcess + && UncachedInputMethodManagerUtils.isThisImeCurrent(context, imm); + if (!isCurrentImeOfCurrentUser) { + final int myPid = Process.myPid(); + Log.i(TAG, "Killing my process: pid=" + myPid); + Process.killProcess(myPid); + } + } +} diff --git a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java index 15b3d8d02..debaad13e 100644 --- a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java @@ -18,7 +18,6 @@ package com.android.inputmethod.latin; import android.content.ContentProviderClient; import android.content.ContentResolver; -import android.content.ContentUris; import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; @@ -29,10 +28,11 @@ import android.provider.UserDictionary.Words; import android.text.TextUtils; import android.util.Log; +import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.compat.UserDictionaryCompatUtils; -import com.android.inputmethod.latin.utils.LocaleUtils; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; +import java.io.File; import java.util.Arrays; import java.util.Locale; @@ -51,23 +51,15 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { // to auto-correct, so we set this to the highest frequency that won't, i.e. 14. private static final int USER_DICT_SHORTCUT_FREQUENCY = 14; - // TODO: use Words.SHORTCUT when we target JellyBean or above - final static String SHORTCUT = "shortcut"; - private static final String[] PROJECTION_QUERY; - static { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - PROJECTION_QUERY = new String[] { - Words.WORD, - SHORTCUT, - Words.FREQUENCY, - }; - } else { - PROJECTION_QUERY = new String[] { - Words.WORD, - Words.FREQUENCY, - }; - } - } + private static final String[] PROJECTION_QUERY_WITH_SHORTCUT = new String[] { + Words.WORD, + Words.SHORTCUT, + Words.FREQUENCY, + }; + private static final String[] PROJECTION_QUERY_WITHOUT_SHORTCUT = new String[] { + Words.WORD, + Words.FREQUENCY, + }; private static final String NAME = "userunigram"; @@ -75,24 +67,18 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { final private String mLocale; final private boolean mAlsoUseMoreRestrictiveLocales; - public UserBinaryDictionary(final Context context, final String locale) { - this(context, locale, false); - } - - public UserBinaryDictionary(final Context context, final String locale, - final boolean alsoUseMoreRestrictiveLocales) { - super(context, getFilenameWithLocale(NAME, locale), Dictionary.TYPE_USER, - false /* isUpdatable */); + protected UserBinaryDictionary(final Context context, final Locale locale, + final boolean alsoUseMoreRestrictiveLocales, final File dictFile, final String name) { + super(context, getDictName(name, locale, dictFile), locale, Dictionary.TYPE_USER, dictFile); if (null == locale) throw new NullPointerException(); // Catch the error earlier - if (SubtypeLocaleUtils.NO_LANGUAGE.equals(locale)) { + final String localeStr = locale.toString(); + if (SubtypeLocaleUtils.NO_LANGUAGE.equals(localeStr)) { // If we don't have a locale, insert into the "all locales" user dictionary. mLocale = USER_DICTIONARY_ALL_LANGUAGES; } else { - mLocale = locale; + mLocale = localeStr; } mAlsoUseMoreRestrictiveLocales = alsoUseMoreRestrictiveLocales; - // Perform a managed query. The Activity will handle closing and re-querying the cursor - // when needed. ContentResolver cres = context.getContentResolver(); mObserver = new ContentObserver(null) { @@ -108,12 +94,18 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { // devices. On older versions of the platform, the hook above will be called instead. @Override public void onChange(final boolean self, final Uri uri) { - setRequiresReload(true); + setNeedsToRecreate(); } }; cres.registerContentObserver(Words.CONTENT_URI, true, mObserver); + reloadDictionaryIfRequired(); + } - loadDictionary(); + @UsedForTesting + public static UserBinaryDictionary getDictionary(final Context context, final Locale locale, + final File dictFile, final String dictNamePrefix) { + return new UserBinaryDictionary(context, locale, false /* alsoUseMoreRestrictiveLocales */, + dictFile, dictNamePrefix + NAME); } @Override @@ -126,7 +118,7 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { } @Override - public void loadDictionaryAsync() { + public void loadInitialContentsLocked() { // Split the locale. For example "en" => ["en"], "de_DE" => ["de", "DE"], // "en_US_foo_bar_qux" => ["en", "US", "foo_bar_qux"] because of the limit of 3. // This is correct for locale processing. @@ -174,11 +166,30 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { } else { requestArguments = localeElements; } + final String requestString = request.toString(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + try { + addWordsFromProjectionLocked(PROJECTION_QUERY_WITH_SHORTCUT, requestString, + requestArguments); + } catch (IllegalArgumentException e) { + // This may happen on some non-compliant devices where the declared API is JB+ but + // the SHORTCUT column is not present for some reason. + addWordsFromProjectionLocked(PROJECTION_QUERY_WITHOUT_SHORTCUT, requestString, + requestArguments); + } + } else { + addWordsFromProjectionLocked(PROJECTION_QUERY_WITHOUT_SHORTCUT, requestString, + requestArguments); + } + } + + private void addWordsFromProjectionLocked(final String[] query, String request, + final String[] requestArguments) throws IllegalArgumentException { Cursor cursor = null; try { cursor = mContext.getContentResolver().query( - Words.CONTENT_URI, PROJECTION_QUERY, request.toString(), requestArguments, null); - addWords(cursor); + Words.CONTENT_URI, query, request, requestArguments, null); + addWordsLocked(cursor); } catch (final SQLiteException e) { Log.e(TAG, "SQLiteException in the remote User dictionary process.", e); } finally { @@ -190,8 +201,8 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { } } - public boolean isEnabled() { - final ContentResolver cr = mContext.getContentResolver(); + public static boolean isEnabled(final Context context) { + final ContentResolver cr = context.getContentResolver(); final ContentProviderClient client = cr.acquireContentProviderClient(Words.CONTENT_URI); if (client != null) { client.release(); @@ -204,18 +215,15 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { /** * Adds a word to the user dictionary and makes it persistent. * + * @param context the context + * @param locale the locale * @param word the word to add. If the word is capitalized, then the dictionary will * recognize it as a capitalized word when searched. */ - public synchronized void addWordToUserDictionary(final String word) { + public static void addWordToUserDictionary(final Context context, final Locale locale, + final String word) { // Update the user dictionary provider - final Locale locale; - if (USER_DICTIONARY_ALL_LANGUAGES == mLocale) { - locale = null; - } else { - locale = LocaleUtils.constructLocaleFromString(mLocale); - } - UserDictionaryCompatUtils.addWord(mContext, word, + UserDictionaryCompatUtils.addWord(context, word, HISTORICAL_DEFAULT_USER_DICTIONARY_FREQUENCY, null, locale); } @@ -232,12 +240,12 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { } } - private void addWords(final Cursor cursor) { + private void addWordsLocked(final Cursor cursor) { final boolean hasShortcutColumn = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; if (cursor == null) return; if (cursor.moveToFirst()) { final int indexWord = cursor.getColumnIndex(Words.WORD); - final int indexShortcut = hasShortcutColumn ? cursor.getColumnIndex(SHORTCUT) : 0; + final int indexShortcut = hasShortcutColumn ? cursor.getColumnIndex(Words.SHORTCUT) : 0; final int indexFrequency = cursor.getColumnIndex(Words.FREQUENCY); while (!cursor.isAfterLast()) { final String word = cursor.getString(indexWord); @@ -246,25 +254,19 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { final int adjustedFrequency = scaleFrequencyFromDefaultToLatinIme(frequency); // Safeguard against adding really long words. if (word.length() < MAX_WORD_LENGTH) { - super.addWord(word, null, adjustedFrequency, 0 /* shortcutFreq */, - false /* isNotAWord */); - } - if (null != shortcut && shortcut.length() < MAX_WORD_LENGTH) { - super.addWord(shortcut, word, adjustedFrequency, USER_DICT_SHORTCUT_FREQUENCY, - true /* isNotAWord */); + runGCIfRequiredLocked(true /* mindsBlockByGC */); + addUnigramLocked(word, adjustedFrequency, null /* shortcutTarget */, + 0 /* shortcutFreq */, false /* isNotAWord */, + false /* isBlacklisted */, BinaryDictionary.NOT_A_VALID_TIMESTAMP); + if (null != shortcut && shortcut.length() < MAX_WORD_LENGTH) { + runGCIfRequiredLocked(true /* mindsBlockByGC */); + addUnigramLocked(shortcut, adjustedFrequency, word, + USER_DICT_SHORTCUT_FREQUENCY, true /* isNotAWord */, + false /* isBlacklisted */, BinaryDictionary.NOT_A_VALID_TIMESTAMP); + } } cursor.moveToNext(); } } } - - @Override - protected boolean hasContentChanged() { - return true; - } - - @Override - protected boolean needsToReloadBeforeWriting() { - return true; - } } diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java index 039dadc66..21fe7e0eb 100644 --- a/java/src/com/android/inputmethod/latin/WordComposer.java +++ b/java/src/com/android/inputmethod/latin/WordComposer.java @@ -16,11 +16,13 @@ package com.android.inputmethod.latin; -import com.android.inputmethod.keyboard.Key; -import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.event.CombinerChain; +import com.android.inputmethod.event.Event; +import com.android.inputmethod.latin.utils.CoordinateUtils; import com.android.inputmethod.latin.utils.StringUtils; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collections; /** * A place to store the currently composing word with information such as adjacent key codes as well @@ -37,17 +39,12 @@ public final class WordComposer { public static final int CAPS_MODE_AUTO_SHIFTED = 0x5; public static final int CAPS_MODE_AUTO_SHIFT_LOCKED = 0x7; - // An array of code points representing the characters typed so far. - // The array is limited to MAX_WORD_LENGTH code points, but mTypedWord extends past that - // and mCodePointSize can go past that. If mCodePointSize is greater than MAX_WORD_LENGTH, - // this just does not contain the associated code points past MAX_WORD_LENGTH. - private int[] mPrimaryKeyCodes; + private CombinerChain mCombinerChain; + private String mCombiningSpec; // Memory so that we don't uselessly recreate the combiner chain + + // The list of events that served to compose this string. + private final ArrayList<Event> mEvents; private final InputPointers mInputPointers = new InputPointers(MAX_WORD_LENGTH); - // This is the typed word, as a StringBuilder. This has the same contents as mPrimaryKeyCodes - // but under a StringBuilder representation for ease of use, depending on what is more useful - // at any given time. However this is not limited in size, while mPrimaryKeyCodes is limited - // to MAX_WORD_LENGTH code points. - private final StringBuilder mTypedWord; private String mAutoCorrection; private boolean mIsResumed; private boolean mIsBatchMode; @@ -60,10 +57,10 @@ public final class WordComposer { private String mRejectedBatchModeSuggestion; // Cache these values for performance + private CharSequence mTypedWordCache; private int mCapsCount; private int mDigitsCount; private int mCapitalizedMode; - private int mTrailingSingleQuotesCount; // This is the number of code points entered so far. This is not limited to MAX_WORD_LENGTH. // In general, this contains the size of mPrimaryKeyCodes, except when this is greater than // MAX_WORD_LENGTH in which case mPrimaryKeyCodes only contain the first MAX_WORD_LENGTH @@ -72,129 +69,156 @@ public final class WordComposer { private int mCursorPositionWithinWord; /** - * Whether the user chose to capitalize the first char of the word. + * Whether the composing word has the only first char capitalized. */ - private boolean mIsFirstCharCapitalized; + private boolean mIsOnlyFirstCharCapitalized; public WordComposer() { - mPrimaryKeyCodes = new int[MAX_WORD_LENGTH]; - mTypedWord = new StringBuilder(MAX_WORD_LENGTH); + mCombinerChain = new CombinerChain(""); + mEvents = new ArrayList<>(); mAutoCorrection = null; - mTrailingSingleQuotesCount = 0; mIsResumed = false; mIsBatchMode = false; mCursorPositionWithinWord = 0; mRejectedBatchModeSuggestion = null; - refreshSize(); + refreshTypedWordCache(); } - public WordComposer(final WordComposer source) { - mPrimaryKeyCodes = Arrays.copyOf(source.mPrimaryKeyCodes, source.mPrimaryKeyCodes.length); - mTypedWord = new StringBuilder(source.mTypedWord); - mInputPointers.copy(source.mInputPointers); - mCapsCount = source.mCapsCount; - mDigitsCount = source.mDigitsCount; - mIsFirstCharCapitalized = source.mIsFirstCharCapitalized; - mCapitalizedMode = source.mCapitalizedMode; - mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount; - mIsResumed = source.mIsResumed; - mIsBatchMode = source.mIsBatchMode; - mCursorPositionWithinWord = source.mCursorPositionWithinWord; - mRejectedBatchModeSuggestion = source.mRejectedBatchModeSuggestion; - refreshSize(); + /** + * Restart the combiners, possibly with a new spec. + * @param combiningSpec The spec string for combining. This is found in the extra value. + */ + public void restartCombining(final String combiningSpec) { + final String nonNullCombiningSpec = null == combiningSpec ? "" : combiningSpec; + if (!nonNullCombiningSpec.equals(mCombiningSpec)) { + mCombinerChain = new CombinerChain( + mCombinerChain.getComposingWordWithCombiningFeedback().toString(), + CombinerChain.createCombiners(nonNullCombiningSpec)); + mCombiningSpec = nonNullCombiningSpec; + } } /** * Clear out the keys registered so far. */ public void reset() { - mTypedWord.setLength(0); + mCombinerChain.reset(); + mEvents.clear(); mAutoCorrection = null; mCapsCount = 0; mDigitsCount = 0; - mIsFirstCharCapitalized = false; - mTrailingSingleQuotesCount = 0; + mIsOnlyFirstCharCapitalized = false; mIsResumed = false; mIsBatchMode = false; mCursorPositionWithinWord = 0; mRejectedBatchModeSuggestion = null; - refreshSize(); + refreshTypedWordCache(); } - private final void refreshSize() { - mCodePointSize = mTypedWord.codePointCount(0, mTypedWord.length()); + private final void refreshTypedWordCache() { + mTypedWordCache = mCombinerChain.getComposingWordWithCombiningFeedback(); + mCodePointSize = Character.codePointCount(mTypedWordCache, 0, mTypedWordCache.length()); } /** * Number of keystrokes in the composing word. * @return the number of keystrokes */ - public final int size() { + // This may be made public if need be, but right now it's not used anywhere + /* package for tests */ int size() { return mCodePointSize; } - public final boolean isComposingWord() { - return size() > 0; - } + /** + * Copy the code points in the typed word to a destination array of ints. + * + * If the array is too small to hold the code points in the typed word, nothing is copied and + * -1 is returned. + * + * @param destination the array of ints. + * @return the number of copied code points. + */ + public int copyCodePointsExceptTrailingSingleQuotesAndReturnCodePointCount( + final int[] destination) { + // This method can be called on a separate thread and mTypedWordCache can change while we + // are executing this method. + final String typedWord = mTypedWordCache.toString(); + // lastIndex is exclusive + final int lastIndex = typedWord.length() + - StringUtils.getTrailingSingleQuotesCount(typedWord); + if (lastIndex <= 0) { + // The string is empty or contains only single quotes. + return 0; + } - // TODO: make sure that the index should not exceed MAX_WORD_LENGTH - public int getCodeAt(int index) { - if (index >= MAX_WORD_LENGTH) { + // The following function counts the number of code points in the text range which begins + // at index 0 and extends to the character at lastIndex. + final int codePointSize = Character.codePointCount(typedWord, 0, lastIndex); + if (codePointSize > destination.length) { return -1; } - return mPrimaryKeyCodes[index]; + return StringUtils.copyCodePointsAndReturnCodePointCount(destination, typedWord, 0, + lastIndex, true /* downCase */); } - public int getCodeBeforeCursor() { - if (mCursorPositionWithinWord < 1 || mCursorPositionWithinWord > mPrimaryKeyCodes.length) { - return Constants.NOT_A_CODE; - } - return mPrimaryKeyCodes[mCursorPositionWithinWord - 1]; + public boolean isSingleLetter() { + return size() == 1; } - public InputPointers getInputPointers() { - return mInputPointers; + public final boolean isComposingWord() { + return size() > 0; } - private static boolean isFirstCharCapitalized(final int index, final int codePoint, - final boolean previous) { - if (index == 0) return Character.isUpperCase(codePoint); - return previous && !Character.isUpperCase(codePoint); + public InputPointers getInputPointers() { + return mInputPointers; } /** - * Add a new keystroke, with the pressed key's code point with the touch point coordinates. + * Process an input event. + * + * All input events should be supported, including software/hardware events, characters as well + * as deletions, multiple inputs and gestures. + * + * @param event the event to process. */ - public void add(final int primaryCode, final int keyX, final int keyY) { + public void processEvent(final Event event) { + final int primaryCode = event.mCodePoint; + final int keyX = event.mX; + final int keyY = event.mY; final int newIndex = size(); - mTypedWord.appendCodePoint(primaryCode); - refreshSize(); + mCombinerChain.processEvent(mEvents, event); + mEvents.add(event); + refreshTypedWordCache(); mCursorPositionWithinWord = mCodePointSize; - if (newIndex < MAX_WORD_LENGTH) { - mPrimaryKeyCodes[newIndex] = primaryCode >= Constants.CODE_SPACE - ? Character.toLowerCase(primaryCode) : primaryCode; - // In the batch input mode, the {@code mInputPointers} holds batch input points and - // shouldn't be overridden by the "typed key" coordinates - // (See {@link #setBatchInputWord}). - if (!mIsBatchMode) { - // TODO: Set correct pointer id and time - mInputPointers.addPointer(newIndex, keyX, keyY, 0, 0); - } + // We may have deleted the last one. + if (0 == mCodePointSize) { + mIsOnlyFirstCharCapitalized = false; } - mIsFirstCharCapitalized = isFirstCharCapitalized( - newIndex, primaryCode, mIsFirstCharCapitalized); - if (Character.isUpperCase(primaryCode)) mCapsCount++; - if (Character.isDigit(primaryCode)) mDigitsCount++; - if (Constants.CODE_SINGLE_QUOTE == primaryCode) { - ++mTrailingSingleQuotesCount; - } else { - mTrailingSingleQuotesCount = 0; + if (Constants.CODE_DELETE != event.mKeyCode) { + if (newIndex < MAX_WORD_LENGTH) { + // In the batch input mode, the {@code mInputPointers} holds batch input points and + // shouldn't be overridden by the "typed key" coordinates + // (See {@link #setBatchInputWord}). + if (!mIsBatchMode) { + // TODO: Set correct pointer id and time + mInputPointers.addPointerAt(newIndex, keyX, keyY, 0, 0); + } + } + if (0 == newIndex) { + mIsOnlyFirstCharCapitalized = Character.isUpperCase(primaryCode); + } else { + mIsOnlyFirstCharCapitalized = mIsOnlyFirstCharCapitalized + && !Character.isUpperCase(primaryCode); + } + if (Character.isUpperCase(primaryCode)) mCapsCount++; + if (Character.isDigit(primaryCode)) mDigitsCount++; } mAutoCorrection = null; } public void setCursorPositionWithinWord(final int posWithinWord) { mCursorPositionWithinWord = posWithinWord; + // TODO: compute where that puts us inside the events } public boolean isCursorFrontOrMiddleOfComposingWord() { @@ -215,17 +239,12 @@ public final class WordComposer { * @return true if the cursor is still inside the composing word, false otherwise. */ public boolean moveCursorByAndReturnIfInsideComposingWord(final int expectedMoveAmount) { + // TODO: should uncommit the composing feedback + mCombinerChain.reset(); int actualMoveAmountWithinWord = 0; int cursorPos = mCursorPositionWithinWord; - final int[] codePoints; - if (mCodePointSize >= MAX_WORD_LENGTH) { - // If we have more than MAX_WORD_LENGTH characters, we don't have everything inside - // mPrimaryKeyCodes. This should be rare enough that we can afford to just compute - // the array on the fly when this happens. - codePoints = StringUtils.toCodePointArray(mTypedWord.toString()); - } else { - codePoints = mPrimaryKeyCodes; - } + // TODO: Don't make that copy. We can do this directly from mTypedWordCache. + final int[] codePoints = StringUtils.toCodePointArray(mTypedWordCache); if (expectedMoveAmount >= 0) { // Moving the cursor forward for the expected amount or until the end of the word has // been reached, whichever comes first. @@ -261,98 +280,48 @@ public final class WordComposer { final int codePoint = Character.codePointAt(word, i); // We don't want to override the batch input points that are held in mInputPointers // (See {@link #add(int,int,int)}). - add(codePoint, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); + processEvent(Event.createEventForCodePointFromUnknownSource(codePoint)); } } /** - * Add a dummy key by retrieving reasonable coordinates - */ - public void addKeyInfo(final int codePoint, final Keyboard keyboard) { - final int x, y; - final Key key; - if (keyboard != null && (key = keyboard.getKey(codePoint)) != null) { - x = key.getX() + key.getWidth() / 2; - y = key.getY() + key.getHeight() / 2; - } else { - x = Constants.NOT_A_COORDINATE; - y = Constants.NOT_A_COORDINATE; - } - add(codePoint, x, y); - } - - /** * Set the currently composing word to the one passed as an argument. * This will register NOT_A_COORDINATE for X and Ys, and use the passed keyboard for proximity. + * @param codePoints the code points to set as the composing word. + * @param coordinates the x, y coordinates of the key in the CoordinateUtils format */ - public void setComposingWord(final CharSequence word, final Keyboard keyboard) { + public void setComposingWord(final int[] codePoints, final int[] coordinates) { reset(); - final int length = word.length(); - for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) { - final int codePoint = Character.codePointAt(word, i); - addKeyInfo(codePoint, keyboard); + final int length = codePoints.length; + for (int i = 0; i < length; ++i) { + processEvent(Event.createEventForCodePointFromAlreadyTypedText(codePoints[i], + CoordinateUtils.xFromArray(coordinates, i), + CoordinateUtils.yFromArray(coordinates, i))); } mIsResumed = true; } /** - * Delete the last keystroke as a result of hitting backspace. - */ - public void deleteLast() { - final int size = size(); - if (size > 0) { - // Note: mTypedWord.length() and mCodes.length differ when there are surrogate pairs - final int stringBuilderLength = mTypedWord.length(); - if (stringBuilderLength < size) { - throw new RuntimeException( - "In WordComposer: mCodes and mTypedWords have non-matching lengths"); - } - final int lastChar = mTypedWord.codePointBefore(stringBuilderLength); - if (Character.isSupplementaryCodePoint(lastChar)) { - mTypedWord.delete(stringBuilderLength - 2, stringBuilderLength); - } else { - mTypedWord.deleteCharAt(stringBuilderLength - 1); - } - if (Character.isUpperCase(lastChar)) mCapsCount--; - if (Character.isDigit(lastChar)) mDigitsCount--; - refreshSize(); - } - // We may have deleted the last one. - if (0 == size()) { - mIsFirstCharCapitalized = false; - } - if (mTrailingSingleQuotesCount > 0) { - --mTrailingSingleQuotesCount; - } else { - int i = mTypedWord.length(); - while (i > 0) { - i = mTypedWord.offsetByCodePoints(i, -1); - if (Constants.CODE_SINGLE_QUOTE != mTypedWord.codePointAt(i)) break; - ++mTrailingSingleQuotesCount; - } - } - mCursorPositionWithinWord = mCodePointSize; - mAutoCorrection = null; - } - - /** * Returns the word as it was typed, without any correction applied. * @return the word that was typed so far. Never returns null. */ public String getTypedWord() { - return mTypedWord.toString(); + return mTypedWordCache.toString(); } /** - * Whether or not the user typed a capital letter as the first letter in the word + * Whether this composer is composing or about to compose a word in which only the first letter + * is a capital. + * + * If we do have a composing word, we just return whether the word has indeed only its first + * character capitalized. If we don't, then we return a value based on the capitalized mode, + * which tell us what is likely to happen for the next composing word. + * * @return capitalization preference */ - public boolean isFirstCharCapitalized() { - return mIsFirstCharCapitalized; - } - - public int trailingSingleQuotesCount() { - return mTrailingSingleQuotesCount; + public boolean isOrWillBeOnlyFirstCharCapitalized() { + return isComposingWord() ? mIsOnlyFirstCharCapitalized + : (CAPS_MODE_OFF != mCapitalizedMode); } /** @@ -390,10 +359,10 @@ public final class WordComposer { /** * 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. + * WordComposer needs to know about the caps mode for several reasons. The first is, we need + * to know after the fact what the reason was, to register the correct form into the user + * history dictionary: if the word was automatically capitalized, we should insert it in + * all-lower case but if it's a manual pressing of shift, then it should be inserted as is. * Also, batch input needs to know about the current caps mode to display correctly * capitalized suggestions. * @param mode the mode at the time of start @@ -403,6 +372,20 @@ public final class WordComposer { } /** + * Before fetching suggestions, we don't necessarily know about the capitalized mode yet. + * + * If we don't have a composing word yet, we take a note of this mode so that we can then + * supply this information to the suggestion process. If we have a composing word, then + * the previous mode has priority over this. + * @param mode the mode just before fetching suggestions + */ + public void adviseCapitalizedModeBeforeFetchingSuggestions(final int mode) { + if (!isComposingWord()) { + mCapitalizedMode = mode; + } + } + + /** * Returns whether the word was automatically capitalized. * @return whether the word was automatically capitalized */ @@ -433,16 +416,15 @@ public final class WordComposer { } // `type' should be one of the LastComposedWord.COMMIT_TYPE_* constants above. - public LastComposedWord commitWord(final int type, final String committedWord, - final String separatorString, final String prevWord) { + // committedWord should contain suggestion spans if applicable. + public LastComposedWord commitWord(final int type, final CharSequence committedWord, + final String separatorString, final PrevWordsInfo prevWordsInfo) { // Note: currently, we come here whenever we commit a word. If it's a MANUAL_PICK // or a DECIDED_WORD we may cancel the commit later; otherwise, we should deactivate // the last composed word to ensure this does not happen. - final int[] primaryKeyCodes = mPrimaryKeyCodes; - mPrimaryKeyCodes = new int[MAX_WORD_LENGTH]; - final LastComposedWord lastComposedWord = new LastComposedWord(primaryKeyCodes, - mInputPointers, mTypedWord.toString(), committedWord, separatorString, - prevWord, mCapitalizedMode); + final LastComposedWord lastComposedWord = new LastComposedWord(mEvents, + mInputPointers, mTypedWordCache.toString(), committedWord, separatorString, + prevWordsInfo, mCapitalizedMode); mInputPointers.reset(); if (type != LastComposedWord.COMMIT_TYPE_DECIDED_WORD && type != LastComposedWord.COMMIT_TYPE_MANUAL_PICK) { @@ -451,12 +433,12 @@ public final class WordComposer { mCapsCount = 0; mDigitsCount = 0; mIsBatchMode = false; - mTypedWord.setLength(0); + mCombinerChain.reset(); + mEvents.clear(); mCodePointSize = 0; - mTrailingSingleQuotesCount = 0; - mIsFirstCharCapitalized = false; + mIsOnlyFirstCharCapitalized = false; mCapitalizedMode = CAPS_MODE_OFF; - refreshSize(); + refreshTypedWordCache(); mAutoCorrection = null; mCursorPositionWithinWord = 0; mIsResumed = false; @@ -465,11 +447,11 @@ public final class WordComposer { } public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord) { - mPrimaryKeyCodes = lastComposedWord.mPrimaryKeyCodes; + mEvents.clear(); + Collections.copy(mEvents, lastComposedWord.mEvents); mInputPointers.set(lastComposedWord.mInputPointers); - mTypedWord.setLength(0); - mTypedWord.append(lastComposedWord.mTypedWord); - refreshSize(); + mCombinerChain.reset(); + refreshTypedWordCache(); mCapitalizedMode = lastComposedWord.mCapitalizedMode; mAutoCorrection = null; // This will be filled by the next call to updateSuggestion. mCursorPositionWithinWord = mCodePointSize; diff --git a/java/src/com/android/inputmethod/latin/WordListInfo.java b/java/src/com/android/inputmethod/latin/WordListInfo.java index 5ac806a0c..268fe9818 100644 --- a/java/src/com/android/inputmethod/latin/WordListInfo.java +++ b/java/src/com/android/inputmethod/latin/WordListInfo.java @@ -22,8 +22,10 @@ package com.android.inputmethod.latin; public final class WordListInfo { public final String mId; public final String mLocale; - public WordListInfo(final String id, final String locale) { + public final String mRawChecksum; + public WordListInfo(final String id, final String locale, final String rawChecksum) { mId = id; mLocale = locale; + mRawChecksum = rawChecksum; } } diff --git a/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java b/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java index 028f78a87..7071d8689 100644 --- a/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java +++ b/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java @@ -26,8 +26,8 @@ import android.os.Environment; import com.android.inputmethod.latin.BinaryDictionaryFileDumper; import com.android.inputmethod.latin.BinaryDictionaryGetter; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; -import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.latin.makedict.DictionaryHeader; +import com.android.inputmethod.latin.utils.DialogUtils; import com.android.inputmethod.latin.utils.DictionaryInfoUtils; import com.android.inputmethod.latin.utils.LocaleUtils; @@ -49,9 +49,9 @@ public class ExternalDictionaryGetterForDebug { private static String[] findDictionariesInTheDownloadedFolder() { final File[] files = new File(SOURCE_FOLDER).listFiles(); - final ArrayList<String> eligibleList = CollectionUtils.newArrayList(); + final ArrayList<String> eligibleList = new ArrayList<>(); for (File f : files) { - final FileHeader header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(f); + final DictionaryHeader header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(f); if (null == header) continue; eligibleList.add(f.getName()); } @@ -70,7 +70,7 @@ public class ExternalDictionaryGetterForDebug { } private static void showNoFileDialog(final Context context) { - new AlertDialog.Builder(context) + new AlertDialog.Builder(DialogUtils.getPlatformDialogThemeContext(context)) .setMessage(R.string.read_external_dictionary_no_files_message) .setPositiveButton(android.R.string.ok, new OnClickListener() { @Override @@ -81,8 +81,8 @@ public class ExternalDictionaryGetterForDebug { } private static void showChooseFileDialog(final Context context, final String[] fileNames) { - final AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.read_external_dictionary_multiple_files_title) + new AlertDialog.Builder(DialogUtils.getPlatformDialogThemeContext(context)) + .setTitle(R.string.read_external_dictionary_multiple_files_title) .setItems(fileNames, new OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int which) { @@ -99,7 +99,7 @@ public class ExternalDictionaryGetterForDebug { public static void askInstallFile(final Context context, final String dirPath, final String fileName, final Runnable completeRunnable) { final File file = new File(dirPath, fileName.toString()); - final FileHeader header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(file); + final DictionaryHeader header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(file); final StringBuilder message = new StringBuilder(); final String locale = header.getLocaleString(); for (String key : header.mDictionaryOptions.mAttributes.keySet()) { @@ -111,7 +111,7 @@ public class ExternalDictionaryGetterForDebug { final String title = String.format( context.getString(R.string.read_external_dictionary_confirm_install_message), languageName); - new AlertDialog.Builder(context) + new AlertDialog.Builder(DialogUtils.getPlatformDialogThemeContext(context)) .setTitle(title) .setMessage(message) .setNegativeButton(android.R.string.cancel, new OnClickListener() { @@ -143,7 +143,7 @@ public class ExternalDictionaryGetterForDebug { } private static void installFile(final Context context, final File file, - final FileHeader header) { + final DictionaryHeader header) { BufferedOutputStream outputStream = null; File tempFile = null; try { @@ -167,7 +167,7 @@ public class ExternalDictionaryGetterForDebug { } } catch (IOException e) { // There was an error: show a dialog - new AlertDialog.Builder(context) + new AlertDialog.Builder(DialogUtils.getPlatformDialogThemeContext(context)) .setTitle(R.string.error) .setMessage(e.toString()) .setPositiveButton(android.R.string.ok, new OnClickListener() { diff --git a/java/src/com/android/inputmethod/latin/define/ProductionFlag.java b/java/src/com/android/inputmethod/latin/define/ProductionFlag.java index dc937fb25..972580298 100644 --- a/java/src/com/android/inputmethod/latin/define/ProductionFlag.java +++ b/java/src/com/android/inputmethod/latin/define/ProductionFlag.java @@ -21,12 +21,17 @@ public final class ProductionFlag { // This class is not publicly instantiable. } - public static final boolean USES_DEVELOPMENT_ONLY_DIAGNOSTICS = false; + public static final boolean IS_HARDWARE_KEYBOARD_SUPPORTED = false; - // When false, USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG suggests that all guarded - // class-private DEBUG flags should be false, and any privacy controls should be enforced. - // USES_DEVELOPMENT_ONLY_DIAGNOSTICS must be false for any production build. - public static final boolean USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG = false; + // When true, enable {@link InputMethodService#onUpdateCursor} callback with + // {@link InputMethodService#setCursorAnchorMonitorMode}, which is not yet available in + // API level 19. Do not turn this on in production until the new API becomes publicly + // available. + public static final boolean USES_CURSOR_ANCHOR_MONITOR = false; - public static final boolean IS_HARDWARE_KEYBOARD_SUPPORTED = false; + // Include all suggestions from all dictionaries in {@link SuggestedWords#mRawSuggestions}. + public static final boolean INCLUDE_RAW_SUGGESTIONS = false; + + // When false, the metrics logging is not yet ready to be enabled. + public static final boolean IS_METRICS_LOGGING_SUPPORTED = false; } diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java new file mode 100644 index 000000000..5ab7db8ce --- /dev/null +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java @@ -0,0 +1,2006 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.inputlogic; + +import android.os.SystemClock; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.SuggestionSpan; +import android.util.Log; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.inputmethod.CorrectionInfo; +import android.view.inputmethod.EditorInfo; + +import com.android.inputmethod.compat.SuggestionSpanUtils; +import com.android.inputmethod.event.Event; +import com.android.inputmethod.event.InputTransaction; +import com.android.inputmethod.keyboard.KeyboardSwitcher; +import com.android.inputmethod.keyboard.ProximityInfo; +import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.Dictionary; +import com.android.inputmethod.latin.DictionaryFacilitator; +import com.android.inputmethod.latin.InputPointers; +import com.android.inputmethod.latin.LastComposedWord; +import com.android.inputmethod.latin.LatinIME; +import com.android.inputmethod.latin.LatinImeLogger; +import com.android.inputmethod.latin.PrevWordsInfo; +import com.android.inputmethod.latin.RichInputConnection; +import com.android.inputmethod.latin.Suggest; +import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback; +import com.android.inputmethod.latin.SuggestedWords; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import com.android.inputmethod.latin.WordComposer; +import com.android.inputmethod.latin.settings.SettingsValues; +import com.android.inputmethod.latin.settings.SpacingAndPunctuations; +import com.android.inputmethod.latin.suggestions.SuggestionStripViewAccessor; +import com.android.inputmethod.latin.utils.AsyncResultHolder; +import com.android.inputmethod.latin.utils.InputTypeUtils; +import com.android.inputmethod.latin.utils.RecapitalizeStatus; +import com.android.inputmethod.latin.utils.StringUtils; +import com.android.inputmethod.latin.utils.TextRange; + +import java.util.ArrayList; +import java.util.TreeSet; +import java.util.concurrent.TimeUnit; + +/** + * This class manages the input logic. + */ +public final class InputLogic { + private static final String TAG = InputLogic.class.getSimpleName(); + + // TODO : Remove this member when we can. + private final LatinIME mLatinIME; + private final SuggestionStripViewAccessor mSuggestionStripViewAccessor; + + // Never null. + private InputLogicHandler mInputLogicHandler = InputLogicHandler.NULL_HANDLER; + + // TODO : make all these fields private as soon as possible. + // Current space state of the input method. This can be any of the above constants. + private int mSpaceState; + // Never null + public SuggestedWords mSuggestedWords = SuggestedWords.EMPTY; + public final Suggest mSuggest; + private final DictionaryFacilitator mDictionaryFacilitator; + + public LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; + // This has package visibility so it can be accessed from InputLogicHandler. + /* package */ final WordComposer mWordComposer; + public final RichInputConnection mConnection; + private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus(); + + private int mDeleteCount; + private long mLastKeyTime; + public final TreeSet<Long> mCurrentlyPressedHardwareKeys = new TreeSet<>(); + + // Keeps track of most recently inserted text (multi-character key) for reverting + private String mEnteredText; + + // TODO: This boolean is persistent state and causes large side effects at unexpected times. + // Find a way to remove it for readability. + private boolean mIsAutoCorrectionIndicatorOn; + private long mDoubleSpacePeriodCountdownStart; + + /** + * Create a new instance of the input logic. + * @param latinIME the instance of the parent LatinIME. We should remove this when we can. + * @param suggestionStripViewAccessor an object to access the suggestion strip view. + * @param dictionaryFacilitator facilitator for getting suggestions and updating user history + * dictionary. + */ + public InputLogic(final LatinIME latinIME, + final SuggestionStripViewAccessor suggestionStripViewAccessor, + final DictionaryFacilitator dictionaryFacilitator) { + mLatinIME = latinIME; + mSuggestionStripViewAccessor = suggestionStripViewAccessor; + mWordComposer = new WordComposer(); + mConnection = new RichInputConnection(latinIME); + mInputLogicHandler = InputLogicHandler.NULL_HANDLER; + mSuggest = new Suggest(dictionaryFacilitator); + mDictionaryFacilitator = dictionaryFacilitator; + } + + /** + * Initializes the input logic for input in an editor. + * + * Call this when input starts or restarts in some editor (typically, in onStartInputView). + * + * @param combiningSpec the combining spec string for this subtype + */ + public void startInput(final String combiningSpec) { + mEnteredText = null; + mWordComposer.restartCombining(combiningSpec); + resetComposingState(true /* alsoResetLastComposedWord */); + mDeleteCount = 0; + mSpaceState = SpaceState.NONE; + mRecapitalizeStatus.disable(); // Do not perform recapitalize until the cursor is moved once + mCurrentlyPressedHardwareKeys.clear(); + mSuggestedWords = SuggestedWords.EMPTY; + // In some cases (namely, after rotation of the device) editorInfo.initialSelStart is lying + // so we try using some heuristics to find out about these and fix them. + mConnection.tryFixLyingCursorPosition(); + cancelDoubleSpacePeriodCountdown(); + if (InputLogicHandler.NULL_HANDLER == mInputLogicHandler) { + mInputLogicHandler = new InputLogicHandler(mLatinIME, this); + } else { + mInputLogicHandler.reset(); + } + } + + /** + * Call this when the subtype changes. + * @param combiningSpec the spec string for the combining rules + */ + public void onSubtypeChanged(final String combiningSpec) { + finishInput(); + startInput(combiningSpec); + } + + /** + * Call this when the orientation changes. + * @param settingsValues the current values of the settings. + */ + public void onOrientationChange(final SettingsValues settingsValues) { + // If !isComposingWord, #commitTyped() is a no-op, but still, it's better to avoid + // the useless IPC of {begin,end}BatchEdit. + if (mWordComposer.isComposingWord()) { + mConnection.beginBatchEdit(); + // If we had a composition in progress, we need to commit the word so that the + // suggestionsSpan will be added. This will allow resuming on the same suggestions + // after rotation is finished. + commitTyped(settingsValues, LastComposedWord.NOT_A_SEPARATOR); + mConnection.endBatchEdit(); + } + } + + /** + * Clean up the input logic after input is finished. + */ + public void finishInput() { + if (mWordComposer.isComposingWord()) { + mConnection.finishComposingText(); + } + resetComposingState(true /* alsoResetLastComposedWord */); + mInputLogicHandler.reset(); + } + + // Normally this class just gets out of scope after the process ends, but in unit tests, we + // create several instances of LatinIME in the same process, which results in several + // instances of InputLogic. This cleans up the associated handler so that tests don't leak + // handlers. + public void recycle() { + final InputLogicHandler inputLogicHandler = mInputLogicHandler; + mInputLogicHandler = InputLogicHandler.NULL_HANDLER; + inputLogicHandler.destroy(); + mDictionaryFacilitator.closeDictionaries(); + } + + /** + * React to a string input. + * + * This is triggered by keys that input many characters at once, like the ".com" key or + * some additional keys for example. + * + * @param settingsValues the current values of the settings. + * @param event the input event containing the data. + * @return the complete transaction object + */ + public InputTransaction onTextInput(final SettingsValues settingsValues, final Event event, + final int keyboardShiftMode, + // TODO: remove this argument + final LatinIME.UIHandler handler) { + final String rawText = event.mText.toString(); + final InputTransaction inputTransaction = new InputTransaction(settingsValues, event, + SystemClock.uptimeMillis(), mSpaceState, + getActualCapsMode(settingsValues, keyboardShiftMode)); + mConnection.beginBatchEdit(); + if (mWordComposer.isComposingWord()) { + commitCurrentAutoCorrection(settingsValues, rawText, handler); + } else { + resetComposingState(true /* alsoResetLastComposedWord */); + } + handler.postUpdateSuggestionStrip(); + final String text = performSpecificTldProcessingOnTextInput(rawText); + if (SpaceState.PHANTOM == mSpaceState) { + promotePhantomSpace(settingsValues); + } + mConnection.commitText(text, 1); + mConnection.endBatchEdit(); + // Space state must be updated before calling updateShiftState + mSpaceState = SpaceState.NONE; + mEnteredText = text; + inputTransaction.setDidAffectContents(); + inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); + return inputTransaction; + } + + /** + * A suggestion was picked from the suggestion strip. + * @param settingsValues the current values of the settings. + * @param suggestionInfo the suggestion info. + * @param keyboardShiftState the shift state of the keyboard, as returned by + * {@link com.android.inputmethod.keyboard.KeyboardSwitcher#getKeyboardShiftMode()} + * @return the complete transaction object + */ + // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener} + // interface + public InputTransaction onPickSuggestionManually(final SettingsValues settingsValues, + final SuggestedWordInfo suggestionInfo, final int keyboardShiftState, + // TODO: remove these arguments + final int currentKeyboardScriptId, final LatinIME.UIHandler handler) { + final SuggestedWords suggestedWords = mSuggestedWords; + final String suggestion = suggestionInfo.mWord; + // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput + if (suggestion.length() == 1 && suggestedWords.isPunctuationSuggestions()) { + // Word separators are suggested before the user inputs something. + // Rely on onCodeInput to do the complicated swapping/stripping logic consistently. + final Event event = Event.createPunctuationSuggestionPickedEvent(suggestionInfo); + return onCodeInput(settingsValues, event, keyboardShiftState, + currentKeyboardScriptId, handler); + } + + final Event event = Event.createSuggestionPickedEvent(suggestionInfo); + final InputTransaction inputTransaction = new InputTransaction(settingsValues, + event, SystemClock.uptimeMillis(), mSpaceState, keyboardShiftState); + // Manual pick affects the contents of the editor, so we take note of this. It's important + // for the sequence of language switching. + inputTransaction.setDidAffectContents(); + mConnection.beginBatchEdit(); + if (SpaceState.PHANTOM == mSpaceState && suggestion.length() > 0 + // In the batch input mode, a manually picked suggested word should just replace + // the current batch input text and there is no need for a phantom space. + && !mWordComposer.isBatchMode()) { + final int firstChar = Character.codePointAt(suggestion, 0); + if (!settingsValues.isWordSeparator(firstChar) + || settingsValues.isUsuallyPrecededBySpace(firstChar)) { + promotePhantomSpace(settingsValues); + } + } + + // TODO: We should not need the following branch. We should be able to take the same + // code path as for other kinds, use commitChosenWord, and do everything normally. We will + // however need to reset the suggestion strip right away, because we know we can't take + // the risk of calling commitCompletion twice because we don't know how the app will react. + if (suggestionInfo.isKindOf(SuggestedWordInfo.KIND_APP_DEFINED)) { + mSuggestedWords = SuggestedWords.EMPTY; + mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); + inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); + resetComposingState(true /* alsoResetLastComposedWord */); + mConnection.commitCompletion(suggestionInfo.mApplicationSpecifiedCompletionInfo); + mConnection.endBatchEdit(); + return inputTransaction; + } + + // We need to log before we commit, because the word composer will store away the user + // typed word. + final String replacedWord = mWordComposer.getTypedWord(); + commitChosenWord(settingsValues, suggestion, + LastComposedWord.COMMIT_TYPE_MANUAL_PICK, LastComposedWord.NOT_A_SEPARATOR); + mConnection.endBatchEdit(); + // Don't allow cancellation of manual pick + mLastComposedWord.deactivate(); + // Space state must be updated before calling updateShiftState + mSpaceState = SpaceState.PHANTOM; + inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); + + // We should show the "Touch again to save" hint if the user pressed the first entry + // AND it's in none of our current dictionaries (main, user or otherwise). + final boolean showingAddToDictionaryHint = + (suggestionInfo.isKindOf(SuggestedWordInfo.KIND_TYPED) + || suggestionInfo.isKindOf(SuggestedWordInfo.KIND_OOV_CORRECTION)) + && !mDictionaryFacilitator.isValidWord(suggestion, true /* ignoreCase */); + + if (showingAddToDictionaryHint && mDictionaryFacilitator.isUserDictionaryEnabled()) { + mSuggestionStripViewAccessor.showAddToDictionaryHint(suggestion); + } else { + // If we're not showing the "Touch again to save", then update the suggestion strip. + handler.postUpdateSuggestionStrip(); + } + return inputTransaction; + } + + /** + * Consider an update to the cursor position. Evaluate whether this update has happened as + * part of normal typing or whether it was an explicit cursor move by the user. In any case, + * do the necessary adjustments. + * @param oldSelStart old selection start + * @param oldSelEnd old selection end + * @param newSelStart new selection start + * @param newSelEnd new selection end + * @return whether the cursor has moved as a result of user interaction. + */ + public boolean onUpdateSelection(final int oldSelStart, final int oldSelEnd, + final int newSelStart, final int newSelEnd) { + if (mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart, oldSelEnd, newSelEnd)) { + return false; + } + // TODO: the following is probably better done in resetEntireInputState(). + // it should only happen when the cursor moved, and the very purpose of the + // test below is to narrow down whether this happened or not. Likewise with + // the call to updateShiftState. + // We set this to NONE because after a cursor move, we don't want the space + // state-related special processing to kick in. + mSpaceState = SpaceState.NONE; + + final boolean selectionChangedOrSafeToReset = + oldSelStart != newSelStart || oldSelEnd != newSelEnd // selection changed + || !mWordComposer.isComposingWord(); // safe to reset + final boolean hasOrHadSelection = (oldSelStart != oldSelEnd || newSelStart != newSelEnd); + final int moveAmount = newSelStart - oldSelStart; + // As an added small gift from the framework, it happens upon rotation when there + // is a selection that we get a wrong cursor position delivered to startInput() that + // does not get reflected in the oldSel{Start,End} parameters to the next call to + // onUpdateSelection. In this case, we may have set a composition, and when we're here + // we realize we shouldn't have. In theory, in this case, selectionChangedOrSafeToReset + // should be true, but that is if the framework had taken that wrong cursor position + // into account, which means we have to reset the entire composing state whenever there + // is or was a selection regardless of whether it changed or not. + if (hasOrHadSelection || (selectionChangedOrSafeToReset + && !mWordComposer.moveCursorByAndReturnIfInsideComposingWord(moveAmount))) { + // If we are composing a word and moving the cursor, we would want to set a + // suggestion span for recorrection to work correctly. Unfortunately, that + // would involve the keyboard committing some new text, which would move the + // cursor back to where it was. Latin IME could then fix the position of the cursor + // again, but the asynchronous nature of the calls results in this wreaking havoc + // with selection on double tap and the like. + // Another option would be to send suggestions each time we set the composing + // text, but that is probably too expensive to do, so we decided to leave things + // as is. + // Also, we're posting a resume suggestions message, and this will update the + // suggestions strip in a few milliseconds, so if we cleared the suggestion strip here + // we'd have the suggestion strip noticeably janky. To avoid that, we don't clear + // it here, which means we'll keep outdated suggestions for a split second but the + // visual result is better. + resetEntireInputState(newSelStart, newSelEnd, false /* clearSuggestionStrip */); + } else { + // resetEntireInputState calls resetCachesUponCursorMove, but forcing the + // composition to end. But in all cases where we don't reset the entire input + // state, we still want to tell the rich input connection about the new cursor + // position so that it can update its caches. + mConnection.resetCachesUponCursorMoveAndReturnSuccess( + newSelStart, newSelEnd, false /* shouldFinishComposition */); + } + + // The cursor has been moved : we now accept to perform recapitalization + mRecapitalizeStatus.enable(); + // We moved the cursor. If we are touching a word, we need to resume suggestion. + mLatinIME.mHandler.postResumeSuggestions(false /* shouldIncludeResumedWordInSuggestions */, + true /* shouldDelay */); + // Stop the last recapitalization, if started. + mRecapitalizeStatus.stop(); + return true; + } + + /** + * React to a code input. It may be a code point to insert, or a symbolic value that influences + * the keyboard behavior. + * + * Typically, this is called whenever a key is pressed on the software keyboard. This is not + * the entry point for gesture input; see the onBatchInput* family of functions for this. + * + * @param settingsValues the current settings values. + * @param event the event to handle. + * @param keyboardShiftMode the current shift mode of the keyboard, as returned by + * {@link com.android.inputmethod.keyboard.KeyboardSwitcher#getKeyboardShiftMode()} + * @return the complete transaction object + */ + public InputTransaction onCodeInput(final SettingsValues settingsValues, final Event event, + final int keyboardShiftMode, + // TODO: remove these arguments + final int currentKeyboardScriptId, final LatinIME.UIHandler handler) { + final InputTransaction inputTransaction = new InputTransaction(settingsValues, event, + SystemClock.uptimeMillis(), mSpaceState, + getActualCapsMode(settingsValues, keyboardShiftMode)); + if (event.mKeyCode != Constants.CODE_DELETE + || inputTransaction.mTimestamp > mLastKeyTime + Constants.LONG_PRESS_MILLISECONDS) { + mDeleteCount = 0; + } + mLastKeyTime = inputTransaction.mTimestamp; + mConnection.beginBatchEdit(); + if (!mWordComposer.isComposingWord()) { + mIsAutoCorrectionIndicatorOn = false; + } + + // TODO: Consolidate the double-space period timer, mLastKeyTime, and the space state. + if (event.mCodePoint != Constants.CODE_SPACE) { + cancelDoubleSpacePeriodCountdown(); + } + + boolean didAutoCorrect = false; + if (event.isFunctionalKeyEvent()) { + // A special key, like delete, shift, emoji, or the settings key. + switch (event.mKeyCode) { + case Constants.CODE_DELETE: + handleBackspace(inputTransaction, currentKeyboardScriptId); + // Backspace is a functional key, but it affects the contents of the editor. + inputTransaction.setDidAffectContents(); + break; + case Constants.CODE_SHIFT: + performRecapitalization(inputTransaction.mSettingsValues); + inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); + if (mSuggestedWords.mIsPrediction) { + inputTransaction.setRequiresUpdateSuggestions(); + } + break; + case Constants.CODE_CAPSLOCK: + // Note: Changing keyboard to shift lock state is handled in + // {@link KeyboardSwitcher#onCodeInput(int)}. + break; + case Constants.CODE_SYMBOL_SHIFT: + // Note: Calling back to the keyboard on the symbol Shift key is handled in + // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}. + break; + case Constants.CODE_SWITCH_ALPHA_SYMBOL: + // Note: Calling back to the keyboard on symbol key is handled in + // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}. + break; + case Constants.CODE_SETTINGS: + onSettingsKeyPressed(); + break; + case Constants.CODE_SHORTCUT: + // We need to switch to the shortcut IME. This is handled by LatinIME since the + // input logic has no business with IME switching. + break; + case Constants.CODE_ACTION_NEXT: + performEditorAction(EditorInfo.IME_ACTION_NEXT); + break; + case Constants.CODE_ACTION_PREVIOUS: + performEditorAction(EditorInfo.IME_ACTION_PREVIOUS); + break; + case Constants.CODE_LANGUAGE_SWITCH: + handleLanguageSwitchKey(); + break; + case Constants.CODE_EMOJI: + // Note: Switching emoji keyboard is being handled in + // {@link KeyboardState#onCodeInput(int,int)}. + break; + case Constants.CODE_ALPHA_FROM_EMOJI: + // Note: Switching back from Emoji keyboard to the main keyboard is being + // handled in {@link KeyboardState#onCodeInput(int,int)}. + break; + case Constants.CODE_SHIFT_ENTER: + // TODO: remove this object + final Event tmpEvent = Event.createSoftwareKeypressEvent(Constants.CODE_ENTER, + event.mKeyCode, event.mX, event.mY, event.isKeyRepeat()); + final InputTransaction tmpTransaction = new InputTransaction( + inputTransaction.mSettingsValues, tmpEvent, + inputTransaction.mTimestamp, inputTransaction.mSpaceState, + inputTransaction.mShiftState); + didAutoCorrect = handleNonSpecialCharacter(tmpTransaction, handler); + // Shift + Enter is treated as a functional key but it results in adding a new + // line, so that does affect the contents of the editor. + inputTransaction.setDidAffectContents(); + break; + default: + throw new RuntimeException("Unknown key code : " + event.mKeyCode); + } + } else { + inputTransaction.setDidAffectContents(); + switch (event.mCodePoint) { + case Constants.CODE_ENTER: + final EditorInfo editorInfo = getCurrentInputEditorInfo(); + final int imeOptionsActionId = + InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo); + if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) { + // Either we have an actionLabel and we should performEditorAction with + // actionId regardless of its value. + performEditorAction(editorInfo.actionId); + } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) { + // We didn't have an actionLabel, but we had another action to execute. + // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast, + // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it + // means there should be an action and the app didn't bother to set a specific + // code for it - presumably it only handles one. It does not have to be treated + // in any specific way: anything that is not IME_ACTION_NONE should be sent to + // performEditorAction. + performEditorAction(imeOptionsActionId); + } else { + // No action label, and the action from imeOptions is NONE: this is a regular + // enter key that should input a carriage return. + didAutoCorrect = handleNonSpecialCharacter(inputTransaction, handler); + } + break; + default: + didAutoCorrect = handleNonSpecialCharacter(inputTransaction, handler); + break; + } + } + if (!didAutoCorrect && event.mKeyCode != Constants.CODE_SHIFT + && event.mKeyCode != Constants.CODE_CAPSLOCK + && event.mKeyCode != Constants.CODE_SWITCH_ALPHA_SYMBOL) + mLastComposedWord.deactivate(); + if (Constants.CODE_DELETE != event.mKeyCode) { + mEnteredText = null; + } + mConnection.endBatchEdit(); + return inputTransaction; + } + + public void onStartBatchInput(final SettingsValues settingsValues, + // TODO: remove these arguments + final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) { + mInputLogicHandler.onStartBatchInput(); + handler.showGesturePreviewAndSuggestionStrip( + SuggestedWords.EMPTY, false /* dismissGestureFloatingPreviewText */); + handler.cancelUpdateSuggestionStrip(); + ++mAutoCommitSequenceNumber; + mConnection.beginBatchEdit(); + if (mWordComposer.isComposingWord()) { + if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { + // If we are in the middle of a recorrection, we need to commit the recorrection + // first so that we can insert the batch input at the current cursor position. + resetEntireInputState(mConnection.getExpectedSelectionStart(), + mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */); + } else if (mWordComposer.isSingleLetter()) { + // We auto-correct the previous (typed, not gestured) string iff it's one character + // long. The reason for this is, even in the middle of gesture typing, you'll still + // tap one-letter words and you want them auto-corrected (typically, "i" in English + // should become "I"). However for any longer word, we assume that the reason for + // tapping probably is that the word you intend to type is not in the dictionary, + // so we do not attempt to correct, on the assumption that if that was a dictionary + // word, the user would probably have gestured instead. + commitCurrentAutoCorrection(settingsValues, LastComposedWord.NOT_A_SEPARATOR, + handler); + } else { + commitTyped(settingsValues, LastComposedWord.NOT_A_SEPARATOR); + } + } + final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); + if (Character.isLetterOrDigit(codePointBeforeCursor) + || settingsValues.isUsuallyFollowedBySpace(codePointBeforeCursor)) { + final boolean autoShiftHasBeenOverriden = keyboardSwitcher.getKeyboardShiftMode() != + getCurrentAutoCapsState(settingsValues); + mSpaceState = SpaceState.PHANTOM; + if (!autoShiftHasBeenOverriden) { + // When we change the space state, we need to update the shift state of the + // keyboard unless it has been overridden manually. This is happening for example + // after typing some letters and a period, then gesturing; the keyboard is not in + // caps mode yet, but since a gesture is starting, it should go in caps mode, + // unless the user explictly said it should not. + keyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(settingsValues), + getCurrentRecapitalizeState()); + } + } + mConnection.endBatchEdit(); + mWordComposer.setCapitalizedModeAtStartComposingTime( + getActualCapsMode(settingsValues, keyboardSwitcher.getKeyboardShiftMode())); + } + + /* The sequence number member is only used in onUpdateBatchInput. It is increased each time + * auto-commit happens. The reason we need this is, when auto-commit happens we trim the + * input pointers that are held in a singleton, and to know how much to trim we rely on the + * results of the suggestion process that is held in mSuggestedWords. + * However, the suggestion process is asynchronous, and sometimes we may enter the + * onUpdateBatchInput method twice without having recomputed suggestions yet, or having + * received new suggestions generated from not-yet-trimmed input pointers. In this case, the + * mIndexOfTouchPointOfSecondWords member will be out of date, and we must not use it lest we + * remove an unrelated number of pointers (possibly even more than are left in the input + * pointers, leading to a crash). + * To avoid that, we increase the sequence number each time we auto-commit and trim the + * input pointers, and we do not use any suggested words that have been generated with an + * earlier sequence number. + */ + private int mAutoCommitSequenceNumber = 1; + public void onUpdateBatchInput(final SettingsValues settingsValues, + final InputPointers batchPointers, + // TODO: remove these arguments + final KeyboardSwitcher keyboardSwitcher) { + if (settingsValues.mPhraseGestureEnabled) { + final SuggestedWordInfo candidate = mSuggestedWords.getAutoCommitCandidate(); + // If these suggested words have been generated with out of date input pointers, then + // we skip auto-commit (see comments above on the mSequenceNumber member). + if (null != candidate + && mSuggestedWords.mSequenceNumber >= mAutoCommitSequenceNumber) { + if (candidate.mSourceDict.shouldAutoCommit(candidate)) { + final String[] commitParts = candidate.mWord.split(Constants.WORD_SEPARATOR, 2); + batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord); + promotePhantomSpace(settingsValues); + mConnection.commitText(commitParts[0], 0); + mSpaceState = SpaceState.PHANTOM; + keyboardSwitcher.requestUpdatingShiftState( + getCurrentAutoCapsState(settingsValues), getCurrentRecapitalizeState()); + mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode( + settingsValues, keyboardSwitcher.getKeyboardShiftMode())); + ++mAutoCommitSequenceNumber; + } + } + } + mInputLogicHandler.onUpdateBatchInput(batchPointers, mAutoCommitSequenceNumber); + } + + public void onEndBatchInput(final InputPointers batchPointers) { + mInputLogicHandler.updateTailBatchInput(batchPointers, mAutoCommitSequenceNumber); + ++mAutoCommitSequenceNumber; + } + + // TODO: remove this argument + public void onCancelBatchInput(final LatinIME.UIHandler handler) { + mInputLogicHandler.onCancelBatchInput(); + handler.showGesturePreviewAndSuggestionStrip( + SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */); + } + + // TODO: on the long term, this method should become private, but it will be difficult. + // Especially, how do we deal with InputMethodService.onDisplayCompletions? + public void setSuggestedWords(final SuggestedWords suggestedWords) { + if (SuggestedWords.EMPTY != suggestedWords) { + final String autoCorrection; + if (suggestedWords.mWillAutoCorrect) { + autoCorrection = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION); + } else { + // We can't use suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD) + // because it may differ from mWordComposer.mTypedWord. + autoCorrection = suggestedWords.mTypedWord; + } + mWordComposer.setAutoCorrection(autoCorrection); + } + mSuggestedWords = suggestedWords; + final boolean newAutoCorrectionIndicator = suggestedWords.mWillAutoCorrect; + // Put a blue underline to a word in TextView which will be auto-corrected. + if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator + && mWordComposer.isComposingWord()) { + mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator; + final CharSequence textWithUnderline = + getTextWithUnderline(mWordComposer.getTypedWord()); + // TODO: when called from an updateSuggestionStrip() call that results from a posted + // message, this is called outside any batch edit. Potentially, this may result in some + // janky flickering of the screen, although the display speed makes it unlikely in + // the practice. + mConnection.setComposingText(textWithUnderline, 1); + } + } + + /** + * Handle inputting a code point to the editor. + * + * Non-special keys are those that generate a single code point. + * This includes all letters, digits, punctuation, separators, emoji. It excludes keys that + * manage keyboard-related stuff like shift, language switch, settings, layout switch, or + * any key that results in multiple code points like the ".com" key. + * + * @param inputTransaction The transaction in progress. + * @return whether this caused an auto-correction to happen. + */ + private boolean handleNonSpecialCharacter(final InputTransaction inputTransaction, + // TODO: remove this argument + final LatinIME.UIHandler handler) { + final int codePoint = inputTransaction.mEvent.mCodePoint; + mSpaceState = SpaceState.NONE; + final boolean didAutoCorrect; + if (inputTransaction.mSettingsValues.isWordSeparator(codePoint) + || Character.getType(codePoint) == Character.OTHER_SYMBOL) { + didAutoCorrect = handleSeparator(inputTransaction, + inputTransaction.mEvent.isSuggestionStripPress(), handler); + } else { + didAutoCorrect = false; + if (SpaceState.PHANTOM == inputTransaction.mSpaceState) { + if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { + // If we are in the middle of a recorrection, we need to commit the recorrection + // first so that we can insert the character at the current cursor position. + resetEntireInputState(mConnection.getExpectedSelectionStart(), + mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */); + } else { + commitTyped(inputTransaction.mSettingsValues, LastComposedWord.NOT_A_SEPARATOR); + } + } + handleNonSeparator(inputTransaction.mSettingsValues, inputTransaction); + } + return didAutoCorrect; + } + + /** + * Handle a non-separator. + * @param settingsValues The current settings values. + * @param inputTransaction The transaction in progress. + */ + private void handleNonSeparator(final SettingsValues settingsValues, + final InputTransaction inputTransaction) { + final int codePoint = inputTransaction.mEvent.mCodePoint; + // TODO: refactor this method to stop flipping isComposingWord around all the time, and + // make it shorter (possibly cut into several pieces). Also factor handleNonSpecialCharacter + // which has the same name as other handle* methods but is not the same. + boolean isComposingWord = mWordComposer.isComposingWord(); + + // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead. + // See onStartBatchInput() to see how to do it. + if (SpaceState.PHANTOM == inputTransaction.mSpaceState + && !settingsValues.isWordConnector(codePoint)) { + if (isComposingWord) { + // Sanity check + throw new RuntimeException("Should not be composing here"); + } + promotePhantomSpace(settingsValues); + } + + if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { + // If we are in the middle of a recorrection, we need to commit the recorrection + // first so that we can insert the character at the current cursor position. + resetEntireInputState(mConnection.getExpectedSelectionStart(), + mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */); + isComposingWord = false; + } + // We want to find out whether to start composing a new word with this character. If so, + // we need to reset the composing state and switch isComposingWord. The order of the + // tests is important for good performance. + // We only start composing if we're not already composing. + if (!isComposingWord + // We only start composing if this is a word code point. Essentially that means it's a + // a letter or a word connector. + && settingsValues.isWordCodePoint(codePoint) + // We never go into composing state if suggestions are not requested. + && settingsValues.isSuggestionsRequested() && + // In languages with spaces, we only start composing a word when we are not already + // touching a word. In languages without spaces, the above conditions are sufficient. + (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations) + || !settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces)) { + // Reset entirely the composing state anyway, then start composing a new word unless + // the character is a word connector. The idea here is, word connectors are not + // separators and they should be treated as normal characters, except in the first + // position where they should not start composing a word. + isComposingWord = !settingsValues.mSpacingAndPunctuations.isWordConnector(codePoint); + // Here we don't need to reset the last composed word. It will be reset + // when we commit this one, if we ever do; if on the other hand we backspace + // it entirely and resume suggestions on the previous word, we'd like to still + // have touch coordinates for it. + resetComposingState(false /* alsoResetLastComposedWord */); + } + if (isComposingWord) { + mWordComposer.processEvent(inputTransaction.mEvent); + // If it's the first letter, make note of auto-caps state + if (mWordComposer.isSingleLetter()) { + mWordComposer.setCapitalizedModeAtStartComposingTime(inputTransaction.mShiftState); + } + mConnection.setComposingText(getTextWithUnderline( + mWordComposer.getTypedWord()), 1); + } else { + final boolean swapWeakSpace = tryStripSpaceAndReturnWhetherShouldSwapInstead( + inputTransaction, inputTransaction.mEvent.isSuggestionStripPress()); + + if (swapWeakSpace && trySwapSwapperAndSpace(inputTransaction)) { + mSpaceState = SpaceState.WEAK; + } else { + sendKeyCodePoint(settingsValues, codePoint); + } + // In case the "add to dictionary" hint was still displayed. + mSuggestionStripViewAccessor.dismissAddToDictionaryHint(); + } + inputTransaction.setRequiresUpdateSuggestions(); + } + + /** + * Handle input of a separator code point. + * @param inputTransaction The transaction in progress. + * @param isFromSuggestionStrip whether this code point comes from the suggestion strip. + * @return whether this caused an auto-correction to happen. + */ + private boolean handleSeparator(final InputTransaction inputTransaction, + final boolean isFromSuggestionStrip, + // TODO: remove this argument + final LatinIME.UIHandler handler) { + final int codePoint = inputTransaction.mEvent.mCodePoint; + final SettingsValues settingsValues = inputTransaction.mSettingsValues; + boolean didAutoCorrect = false; + final boolean wasComposingWord = mWordComposer.isComposingWord(); + // We avoid sending spaces in languages without spaces if we were composing. + final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == codePoint + && !settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces + && wasComposingWord; + if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { + // If we are in the middle of a recorrection, we need to commit the recorrection + // first so that we can insert the separator at the current cursor position. + resetEntireInputState(mConnection.getExpectedSelectionStart(), + mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */); + } + // isComposingWord() may have changed since we stored wasComposing + if (mWordComposer.isComposingWord()) { + if (settingsValues.mAutoCorrectionEnabled) { + final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR + : StringUtils.newSingleCodePointString(codePoint); + commitCurrentAutoCorrection(settingsValues, separator, handler); + didAutoCorrect = true; + } else { + commitTyped(settingsValues, + StringUtils.newSingleCodePointString(codePoint)); + } + } + + final boolean swapWeakSpace = tryStripSpaceAndReturnWhetherShouldSwapInstead( + inputTransaction, isFromSuggestionStrip); + + final boolean isInsideDoubleQuoteOrAfterDigit = Constants.CODE_DOUBLE_QUOTE == codePoint + && mConnection.isInsideDoubleQuoteOrAfterDigit(); + + final boolean needsPrecedingSpace; + if (SpaceState.PHANTOM != inputTransaction.mSpaceState) { + needsPrecedingSpace = false; + } else if (Constants.CODE_DOUBLE_QUOTE == codePoint) { + // Double quotes behave like they are usually preceded by space iff we are + // not inside a double quote or after a digit. + needsPrecedingSpace = !isInsideDoubleQuoteOrAfterDigit; + } else if (settingsValues.mSpacingAndPunctuations.isClusteringSymbol(codePoint) + && settingsValues.mSpacingAndPunctuations.isClusteringSymbol( + mConnection.getCodePointBeforeCursor())) { + needsPrecedingSpace = false; + } else { + needsPrecedingSpace = settingsValues.isUsuallyPrecededBySpace(codePoint); + } + + if (needsPrecedingSpace) { + promotePhantomSpace(settingsValues); + } + + if (tryPerformDoubleSpacePeriod(inputTransaction)) { + mSpaceState = SpaceState.DOUBLE; + inputTransaction.setRequiresUpdateSuggestions(); + } else if (swapWeakSpace && trySwapSwapperAndSpace(inputTransaction)) { + mSpaceState = SpaceState.SWAP_PUNCTUATION; + mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); + } else if (Constants.CODE_SPACE == codePoint) { + if (!mSuggestedWords.isPunctuationSuggestions()) { + mSpaceState = SpaceState.WEAK; + } + + startDoubleSpacePeriodCountdown(inputTransaction); + if (wasComposingWord || mSuggestedWords.isEmpty()) { + inputTransaction.setRequiresUpdateSuggestions(); + } + + if (!shouldAvoidSendingCode) { + sendKeyCodePoint(settingsValues, codePoint); + } + } else { + if ((SpaceState.PHANTOM == inputTransaction.mSpaceState + && settingsValues.isUsuallyFollowedBySpace(codePoint)) + || (Constants.CODE_DOUBLE_QUOTE == codePoint + && isInsideDoubleQuoteOrAfterDigit)) { + // If we are in phantom space state, and the user presses a separator, we want to + // stay in phantom space state so that the next keypress has a chance to add the + // space. For example, if I type "Good dat", pick "day" from the suggestion strip + // then insert a comma and go on to typing the next word, I want the space to be + // inserted automatically before the next word, the same way it is when I don't + // input the comma. A double quote behaves like it's usually followed by space if + // we're inside a double quote. + // The case is a little different if the separator is a space stripper. Such a + // separator does not normally need a space on the right (that's the difference + // between swappers and strippers), so we should not stay in phantom space state if + // the separator is a stripper. Hence the additional test above. + mSpaceState = SpaceState.PHANTOM; + } + + sendKeyCodePoint(settingsValues, codePoint); + + // Set punctuation right away. onUpdateSelection will fire but tests whether it is + // already displayed or not, so it's okay. + mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); + } + + inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); + return didAutoCorrect; + } + + /** + * Handle a press on the backspace key. + * @param inputTransaction The transaction in progress. + */ + private void handleBackspace(final InputTransaction inputTransaction, + // TODO: remove this argument, put it into settingsValues + final int currentKeyboardScriptId) { + mSpaceState = SpaceState.NONE; + mDeleteCount++; + + // In many cases after backspace, we need to update the shift state. Normally we need + // to do this right away to avoid the shift state being out of date in case the user types + // backspace then some other character very fast. However, in the case of backspace key + // repeat, this can lead to flashiness when the cursor flies over positions where the + // shift state should be updated, so if this is a key repeat, we update after a small delay. + // Then again, even in the case of a key repeat, if the cursor is at start of text, it + // can't go any further back, so we can update right away even if it's a key repeat. + final int shiftUpdateKind = + inputTransaction.mEvent.isKeyRepeat() && mConnection.getExpectedSelectionStart() > 0 + ? InputTransaction.SHIFT_UPDATE_LATER : InputTransaction.SHIFT_UPDATE_NOW; + inputTransaction.requireShiftUpdate(shiftUpdateKind); + + if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { + // If we are in the middle of a recorrection, we need to commit the recorrection + // first so that we can remove the character at the current cursor position. + resetEntireInputState(mConnection.getExpectedSelectionStart(), + mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */); + // When we exit this if-clause, mWordComposer.isComposingWord() will return false. + } + if (mWordComposer.isComposingWord()) { + if (mWordComposer.isBatchMode()) { + final String rejectedSuggestion = mWordComposer.getTypedWord(); + mWordComposer.reset(); + mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion); + if (!TextUtils.isEmpty(rejectedSuggestion)) { + mDictionaryFacilitator.removeWordFromPersonalizedDicts(rejectedSuggestion); + } + } else { + mWordComposer.processEvent(inputTransaction.mEvent); + } + if (mWordComposer.isComposingWord()) { + mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); + } else { + mConnection.commitText("", 1); + } + inputTransaction.setRequiresUpdateSuggestions(); + } else { + if (mLastComposedWord.canRevertCommit()) { + revertCommit(inputTransaction); + return; + } + if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) { + // Cancel multi-character input: remove the text we just entered. + // This is triggered on backspace after a key that inputs multiple characters, + // like the smiley key or the .com key. + mConnection.deleteSurroundingText(mEnteredText.length(), 0); + mEnteredText = null; + // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false. + // In addition we know that spaceState is false, and that we should not be + // reverting any autocorrect at this point. So we can safely return. + return; + } + if (SpaceState.DOUBLE == inputTransaction.mSpaceState) { + cancelDoubleSpacePeriodCountdown(); + if (mConnection.revertDoubleSpacePeriod()) { + // No need to reset mSpaceState, it has already be done (that's why we + // receive it as a parameter) + inputTransaction.setRequiresUpdateSuggestions(); + mWordComposer.setCapitalizedModeAtStartComposingTime( + WordComposer.CAPS_MODE_OFF); + return; + } + } else if (SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState) { + if (mConnection.revertSwapPunctuation()) { + // Likewise + return; + } + } + + // No cancelling of commit/double space/swap: we have a regular backspace. + // We should backspace one char and restart suggestion if at the end of a word. + if (mConnection.hasSelection()) { + // If there is a selection, remove it. + final int numCharsDeleted = mConnection.getExpectedSelectionEnd() + - mConnection.getExpectedSelectionStart(); + mConnection.setSelection(mConnection.getExpectedSelectionEnd(), + mConnection.getExpectedSelectionEnd()); + mConnection.deleteSurroundingText(numCharsDeleted, 0); + } else { + // There is no selection, just delete one character. + if (Constants.NOT_A_CURSOR_POSITION == mConnection.getExpectedSelectionEnd()) { + // This should never happen. + Log.e(TAG, "Backspace when we don't know the selection position"); + } + if (inputTransaction.mSettingsValues.isBeforeJellyBean() || + inputTransaction.mSettingsValues.mInputAttributes.isTypeNull()) { + // There are two possible reasons to send a key event: either the field has + // type TYPE_NULL, in which case the keyboard should send events, or we are + // running in backward compatibility mode. Before Jelly bean, the keyboard + // would simulate a hardware keyboard event on pressing enter or delete. This + // is bad for many reasons (there are race conditions with commits) but some + // applications are relying on this behavior so we continue to support it for + // older apps, so we retain this behavior if the app has target SDK < JellyBean. + sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); + if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) { + sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); + } + } else { + final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); + if (codePointBeforeCursor == Constants.NOT_A_CODE) { + // HACK for backward compatibility with broken apps that haven't realized + // yet that hardware keyboards are not the only way of inputting text. + // Nothing to delete before the cursor. We should not do anything, but many + // broken apps expect something to happen in this case so that they can + // catch it and have their broken interface react. If you need the keyboard + // to do this, you're doing it wrong -- please fix your app. + mConnection.deleteSurroundingText(1, 0); + return; + } + final int lengthToDelete = + Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1; + mConnection.deleteSurroundingText(lengthToDelete, 0); + if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) { + final int codePointBeforeCursorToDeleteAgain = + mConnection.getCodePointBeforeCursor(); + if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) { + final int lengthToDeleteAgain = Character.isSupplementaryCodePoint( + codePointBeforeCursorToDeleteAgain) ? 2 : 1; + mConnection.deleteSurroundingText(lengthToDeleteAgain, 0); + } + } + } + } + if (inputTransaction.mSettingsValues + .isCurrentOrientationAllowingSuggestionsPerUserSettings() + && inputTransaction.mSettingsValues.mSpacingAndPunctuations + .mCurrentLanguageHasSpaces + && !mConnection.isCursorFollowedByWordCharacter( + inputTransaction.mSettingsValues.mSpacingAndPunctuations)) { + restartSuggestionsOnWordTouchedByCursor(inputTransaction.mSettingsValues, + true /* shouldIncludeResumedWordInSuggestions */, currentKeyboardScriptId); + } + } + } + + /** + * Handle a press on the language switch key (the "globe key") + */ + private void handleLanguageSwitchKey() { + mLatinIME.switchToNextSubtype(); + } + + /** + * Swap a space with a space-swapping punctuation sign. + * + * This method will check that there are two characters before the cursor and that the first + * one is a space before it does the actual swapping. + * @param inputTransaction The transaction in progress. + * @return true if the swap has been performed, false if it was prevented by preliminary checks. + */ + private boolean trySwapSwapperAndSpace(final InputTransaction inputTransaction) { + final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); + if (Constants.CODE_SPACE != codePointBeforeCursor) { + return false; + } + mConnection.deleteSurroundingText(1, 0); + final String text = inputTransaction.mEvent.getTextToCommit() + " "; + mConnection.commitText(text, 1); + inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); + return true; + } + + /* + * Strip a trailing space if necessary and returns whether it's a swap weak space situation. + * @param inputTransaction The transaction in progress. + * @param isFromSuggestionStrip Whether this code point is coming from the suggestion strip. + * @return whether we should swap the space instead of removing it. + */ + private boolean tryStripSpaceAndReturnWhetherShouldSwapInstead( + final InputTransaction inputTransaction, final boolean isFromSuggestionStrip) { + final int codePoint = inputTransaction.mEvent.mCodePoint; + if (Constants.CODE_ENTER == codePoint && + SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState) { + mConnection.removeTrailingSpace(); + return false; + } + if ((SpaceState.WEAK == inputTransaction.mSpaceState + || SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState) + && isFromSuggestionStrip) { + if (inputTransaction.mSettingsValues.isUsuallyPrecededBySpace(codePoint)) { + return false; + } + if (inputTransaction.mSettingsValues.isUsuallyFollowedBySpace(codePoint)) { + return true; + } + mConnection.removeTrailingSpace(); + } + return false; + } + + public void startDoubleSpacePeriodCountdown(final InputTransaction inputTransaction) { + mDoubleSpacePeriodCountdownStart = inputTransaction.mTimestamp; + } + + public void cancelDoubleSpacePeriodCountdown() { + mDoubleSpacePeriodCountdownStart = 0; + } + + public boolean isDoubleSpacePeriodCountdownActive(final InputTransaction inputTransaction) { + return inputTransaction.mTimestamp - mDoubleSpacePeriodCountdownStart + < inputTransaction.mSettingsValues.mDoubleSpacePeriodTimeout; + } + + /** + * Apply the double-space-to-period transformation if applicable. + * + * The double-space-to-period transformation means that we replace two spaces with a + * period-space sequence of characters. This typically happens when the user presses space + * twice in a row quickly. + * This method will check that the double-space-to-period is active in settings, that the + * two spaces have been input close enough together, that the typed character is a space + * and that the previous character allows for the transformation to take place. If all of + * these conditions are fulfilled, this method applies the transformation and returns true. + * Otherwise, it does nothing and returns false. + * + * @param inputTransaction The transaction in progress. + * @return true if we applied the double-space-to-period transformation, false otherwise. + */ + private boolean tryPerformDoubleSpacePeriod(final InputTransaction inputTransaction) { + // Check the setting, the typed character and the countdown. If any of the conditions is + // not fulfilled, return false. + if (!inputTransaction.mSettingsValues.mUseDoubleSpacePeriod + || Constants.CODE_SPACE != inputTransaction.mEvent.mCodePoint + || !isDoubleSpacePeriodCountdownActive(inputTransaction)) { + return false; + } + // We only do this when we see one space and an accepted code point before the cursor. + // The code point may be a surrogate pair but the space may not, so we need 3 chars. + final CharSequence lastTwo = mConnection.getTextBeforeCursor(3, 0); + if (null == lastTwo) return false; + final int length = lastTwo.length(); + if (length < 2) return false; + if (lastTwo.charAt(length - 1) != Constants.CODE_SPACE) return false; + // We know there is a space in pos -1, and we have at least two chars. If we have only two + // chars, isSurrogatePairs can't return true as charAt(1) is a space, so this is fine. + final int firstCodePoint = + Character.isSurrogatePair(lastTwo.charAt(0), lastTwo.charAt(1)) ? + Character.codePointAt(lastTwo, length - 3) : lastTwo.charAt(length - 2); + if (canBeFollowedByDoubleSpacePeriod(firstCodePoint)) { + cancelDoubleSpacePeriodCountdown(); + mConnection.deleteSurroundingText(1, 0); + final String textToInsert = inputTransaction.mSettingsValues.mSpacingAndPunctuations + .mSentenceSeparatorAndSpace; + mConnection.commitText(textToInsert, 1); + inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); + inputTransaction.setRequiresUpdateSuggestions(); + return true; + } + return false; + } + + /** + * Returns whether this code point can be followed by the double-space-to-period transformation. + * + * See #maybeDoubleSpaceToPeriod for details. + * Generally, most word characters can be followed by the double-space-to-period transformation, + * while most punctuation can't. Some punctuation however does allow for this to take place + * after them, like the closing parenthesis for example. + * + * @param codePoint the code point after which we may want to apply the transformation + * @return whether it's fine to apply the transformation after this code point. + */ + private static boolean canBeFollowedByDoubleSpacePeriod(final int codePoint) { + // TODO: This should probably be a blacklist rather than a whitelist. + // TODO: This should probably be language-dependant... + return Character.isLetterOrDigit(codePoint) + || codePoint == Constants.CODE_SINGLE_QUOTE + || codePoint == Constants.CODE_DOUBLE_QUOTE + || codePoint == Constants.CODE_CLOSING_PARENTHESIS + || codePoint == Constants.CODE_CLOSING_SQUARE_BRACKET + || codePoint == Constants.CODE_CLOSING_CURLY_BRACKET + || codePoint == Constants.CODE_CLOSING_ANGLE_BRACKET + || codePoint == Constants.CODE_PLUS + || codePoint == Constants.CODE_PERCENT + || Character.getType(codePoint) == Character.OTHER_SYMBOL; + } + + /** + * Performs a recapitalization event. + * @param settingsValues The current settings values. + */ + private void performRecapitalization(final SettingsValues settingsValues) { + if (!mConnection.hasSelection() || !mRecapitalizeStatus.mIsEnabled()) { + return; // No selection or recapitalize is disabled for now + } + final int selectionStart = mConnection.getExpectedSelectionStart(); + final int selectionEnd = mConnection.getExpectedSelectionEnd(); + final int numCharsSelected = selectionEnd - selectionStart; + if (numCharsSelected > Constants.MAX_CHARACTERS_FOR_RECAPITALIZATION) { + // We bail out if we have too many characters for performance reasons. We don't want + // to suck possibly multiple-megabyte data. + return; + } + // If we have a recapitalize in progress, use it; otherwise, start a new one. + if (!mRecapitalizeStatus.isStarted() + || !mRecapitalizeStatus.isSetAt(selectionStart, selectionEnd)) { + final CharSequence selectedText = + mConnection.getSelectedText(0 /* flags, 0 for no styles */); + if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection + mRecapitalizeStatus.start(selectionStart, selectionEnd, selectedText.toString(), + settingsValues.mLocale, + settingsValues.mSpacingAndPunctuations.mSortedWordSeparators); + // We trim leading and trailing whitespace. + mRecapitalizeStatus.trim(); + } + mConnection.finishComposingText(); + mRecapitalizeStatus.rotate(); + mConnection.setSelection(selectionEnd, selectionEnd); + mConnection.deleteSurroundingText(numCharsSelected, 0); + mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0); + mConnection.setSelection(mRecapitalizeStatus.getNewCursorStart(), + mRecapitalizeStatus.getNewCursorEnd()); + } + + private void performAdditionToUserHistoryDictionary(final SettingsValues settingsValues, + final String suggestion, final PrevWordsInfo prevWordsInfo) { + // If correction is not enabled, we don't add words to the user history dictionary. + // That's to avoid unintended additions in some sensitive fields, or fields that + // expect to receive non-words. + if (!settingsValues.mAutoCorrectionEnabled) return; + + if (TextUtils.isEmpty(suggestion)) return; + final boolean wasAutoCapitalized = + mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps(); + final int timeStampInSeconds = (int)TimeUnit.MILLISECONDS.toSeconds( + System.currentTimeMillis()); + mDictionaryFacilitator.addToUserHistory(suggestion, wasAutoCapitalized, + prevWordsInfo, timeStampInSeconds, settingsValues.mBlockPotentiallyOffensive); + } + + public void performUpdateSuggestionStripSync(final SettingsValues settingsValues) { + // Check if we have a suggestion engine attached. + if (!settingsValues.isSuggestionsRequested()) { + if (mWordComposer.isComposingWord()) { + Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not " + + "requested!"); + } + // Clear the suggestions strip. + mSuggestionStripViewAccessor.showSuggestionStrip(SuggestedWords.EMPTY); + return; + } + + if (!mWordComposer.isComposingWord() && !settingsValues.mBigramPredictionEnabled) { + mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); + return; + } + + final AsyncResultHolder<SuggestedWords> holder = new AsyncResultHolder<>(); + mInputLogicHandler.getSuggestedWords(Suggest.SESSION_TYPING, + SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() { + @Override + public void onGetSuggestedWords(final SuggestedWords suggestedWords) { + final String typedWord = mWordComposer.getTypedWord(); + // Show new suggestions if we have at least one. Otherwise keep the old + // suggestions with the new typed word. Exception: if the length of the + // typed word is <= 1 (after a deletion typically) we clear old suggestions. + if (suggestedWords.size() > 1 || typedWord.length() <= 1) { + holder.set(suggestedWords); + } else { + holder.set(retrieveOlderSuggestions(typedWord, mSuggestedWords)); + } + } + } + ); + + // This line may cause the current thread to wait. + final SuggestedWords suggestedWords = holder.get(null, + Constants.GET_SUGGESTED_WORDS_TIMEOUT); + if (suggestedWords != null) { + mSuggestionStripViewAccessor.showSuggestionStrip(suggestedWords); + } + } + + /** + * Check if the cursor is touching a word. If so, restart suggestions on this word, else + * do nothing. + * + * @param settingsValues the current values of the settings. + * @param shouldIncludeResumedWordInSuggestions whether to include the word on which we resume + * suggestions in the suggestion list. + */ + // TODO: make this private. + public void restartSuggestionsOnWordTouchedByCursor(final SettingsValues settingsValues, + final boolean shouldIncludeResumedWordInSuggestions, + // TODO: remove this argument, put it into settingsValues + final int currentKeyboardScriptId) { + // HACK: We may want to special-case some apps that exhibit bad behavior in case of + // recorrection. This is a temporary, stopgap measure that will be removed later. + // TODO: remove this. + if (settingsValues.isBrokenByRecorrection() + // Recorrection is not supported in languages without spaces because we don't know + // how to segment them yet. + || !settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces + // If no suggestions are requested, don't try restarting suggestions. + || !settingsValues.isSuggestionsRequested() + // If we are currently in a batch input, we must not resume suggestions, or the result + // of the batch input will replace the new composition. This may happen in the corner case + // that the app moves the cursor on its own accord during a batch input. + || mInputLogicHandler.isInBatchInput() + // If the cursor is not touching a word, or if there is a selection, return right away. + || mConnection.hasSelection() + // If we don't know the cursor location, return. + || mConnection.getExpectedSelectionStart() < 0) { + mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); + return; + } + final int expectedCursorPosition = mConnection.getExpectedSelectionStart(); + if (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations)) { + // Show predictions. + mWordComposer.setCapitalizedModeAtStartComposingTime(WordComposer.CAPS_MODE_OFF); + mLatinIME.mHandler.postUpdateSuggestionStrip(); + return; + } + final TextRange range = mConnection.getWordRangeAtCursor( + settingsValues.mSpacingAndPunctuations.mSortedWordSeparators, + currentKeyboardScriptId); + if (null == range) return; // Happens if we don't have an input connection at all + if (range.length() <= 0) { + // Race condition, or touching a word in a non-supported script. + mLatinIME.setNeutralSuggestionStrip(); + return; + } + // If for some strange reason (editor bug or so) we measure the text before the cursor as + // longer than what the entire text is supposed to be, the safe thing to do is bail out. + if (range.mHasUrlSpans) return; // If there are links, we don't resume suggestions. Making + // edits to a linkified text through batch commands would ruin the URL spans, and unless + // we take very complicated steps to preserve the whole link, we can't do things right so + // we just do not resume because it's safer. + final int numberOfCharsInWordBeforeCursor = range.getNumberOfCharsInWordBeforeCursor(); + if (numberOfCharsInWordBeforeCursor > expectedCursorPosition) return; + final ArrayList<SuggestedWordInfo> suggestions = new ArrayList<>(); + final String typedWord = range.mWord.toString(); + if (shouldIncludeResumedWordInSuggestions) { + suggestions.add(new SuggestedWordInfo(typedWord, + SuggestedWords.MAX_SUGGESTIONS + 1, + SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED, + SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, + SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); + } + if (!isResumableWord(settingsValues, typedWord)) { + mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); + return; + } + int i = 0; + for (final SuggestionSpan span : range.getSuggestionSpansAtWord()) { + for (final String s : span.getSuggestions()) { + ++i; + if (!TextUtils.equals(s, typedWord)) { + suggestions.add(new SuggestedWordInfo(s, + SuggestedWords.MAX_SUGGESTIONS - i, + SuggestedWordInfo.KIND_RESUMED, Dictionary.DICTIONARY_RESUMED, + SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, + SuggestedWordInfo.NOT_A_CONFIDENCE + /* autoCommitFirstWordConfidence */)); + } + } + } + final int[] codePoints = StringUtils.toCodePointArray(typedWord); + // We want the previous word for suggestion. If we have chars in the word + // before the cursor, then we want the word before that, hence 2; otherwise, + // we want the word immediately before the cursor, hence 1. + final PrevWordsInfo prevWordsInfo = getPrevWordsInfoFromNthPreviousWordForSuggestion( + settingsValues.mSpacingAndPunctuations, + 0 == numberOfCharsInWordBeforeCursor ? 1 : 2); + mWordComposer.setComposingWord(codePoints, + mLatinIME.getCoordinatesForCurrentKeyboard(codePoints)); + mWordComposer.setCursorPositionWithinWord( + typedWord.codePointCount(0, numberOfCharsInWordBeforeCursor)); + mConnection.setComposingRegion(expectedCursorPosition - numberOfCharsInWordBeforeCursor, + expectedCursorPosition + range.getNumberOfCharsInWordAfterCursor()); + if (suggestions.size() <= (shouldIncludeResumedWordInSuggestions ? 1 : 0)) { + // If there weren't any suggestion spans on this word, suggestions#size() will be 1 + // if shouldIncludeResumedWordInSuggestions is true, 0 otherwise. In this case, we + // have no useful suggestions, so we will try to compute some for it instead. + mInputLogicHandler.getSuggestedWords(Suggest.SESSION_TYPING, + SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() { + @Override + public void onGetSuggestedWords( + final SuggestedWords suggestedWordsIncludingTypedWord) { + final SuggestedWords suggestedWords; + if (suggestedWordsIncludingTypedWord.size() > 1 + && !shouldIncludeResumedWordInSuggestions) { + // We were able to compute new suggestions for this word. + // Remove the typed word, since we don't want to display it in this + // case. The #getSuggestedWordsExcludingTypedWord() method sets + // willAutoCorrect to false. + suggestedWords = suggestedWordsIncludingTypedWord + .getSuggestedWordsExcludingTypedWord(); + } else { + // No saved suggestions, and we were unable to compute any good one + // either. Rather than displaying an empty suggestion strip, we'll + // display the original word alone in the middle. + // Since there is only one word, willAutoCorrect is false. + suggestedWords = suggestedWordsIncludingTypedWord; + } + mIsAutoCorrectionIndicatorOn = false; + mLatinIME.mHandler.showSuggestionStrip(suggestedWords); + }}); + } else { + // We found suggestion spans in the word. We'll create the SuggestedWords out of + // them, and make willAutoCorrect false. We make typedWordValid false, because the + // color of the word in the suggestion strip changes according to this parameter, + // and false gives the correct color. + final SuggestedWords suggestedWords = new SuggestedWords(suggestions, + null /* rawSuggestions */, typedWord, + false /* typedWordValid */, false /* willAutoCorrect */, + false /* isObsoleteSuggestions */, false /* isPrediction */, + SuggestedWords.NOT_A_SEQUENCE_NUMBER); + mIsAutoCorrectionIndicatorOn = false; + mLatinIME.mHandler.showSuggestionStrip(suggestedWords); + } + } + + /** + * Reverts a previous commit with auto-correction. + * + * This is triggered upon pressing backspace just after a commit with auto-correction. + * + * @param inputTransaction The transaction in progress. + */ + private void revertCommit(final InputTransaction inputTransaction) { + final CharSequence originallyTypedWord = mLastComposedWord.mTypedWord; + final CharSequence committedWord = mLastComposedWord.mCommittedWord; + final String committedWordString = committedWord.toString(); + final int cancelLength = committedWord.length(); + // We want java chars, not codepoints for the following. + final int separatorLength = mLastComposedWord.mSeparatorString.length(); + // TODO: should we check our saved separator against the actual contents of the text view? + final int deleteLength = cancelLength + separatorLength; + if (LatinImeLogger.sDBG) { + if (mWordComposer.isComposingWord()) { + throw new RuntimeException("revertCommit, but we are composing a word"); + } + final CharSequence wordBeforeCursor = + mConnection.getTextBeforeCursor(deleteLength, 0).subSequence(0, cancelLength); + if (!TextUtils.equals(committedWord, wordBeforeCursor)) { + throw new RuntimeException("revertCommit check failed: we thought we were " + + "reverting \"" + committedWord + + "\", but before the cursor we found \"" + wordBeforeCursor + "\""); + } + } + mConnection.deleteSurroundingText(deleteLength, 0); + if (!TextUtils.isEmpty(committedWord)) { + mDictionaryFacilitator.removeWordFromPersonalizedDicts(committedWordString); + } + final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString; + final SpannableString textToCommit = new SpannableString(stringToCommit); + if (committedWord instanceof SpannableString) { + final SpannableString committedWordWithSuggestionSpans = (SpannableString)committedWord; + final Object[] spans = committedWordWithSuggestionSpans.getSpans(0, + committedWord.length(), Object.class); + final int lastCharIndex = textToCommit.length() - 1; + // We will collect all suggestions in the following array. + final ArrayList<String> suggestions = new ArrayList<>(); + // First, add the committed word to the list of suggestions. + suggestions.add(committedWordString); + for (final Object span : spans) { + // If this is a suggestion span, we check that the locale is the right one, and + // that the word is not the committed word. That should mostly be the case. + // Given this, we add it to the list of suggestions, otherwise we discard it. + if (span instanceof SuggestionSpan) { + final SuggestionSpan suggestionSpan = (SuggestionSpan)span; + if (!suggestionSpan.getLocale().equals( + inputTransaction.mSettingsValues.mLocale.toString())) { + continue; + } + for (final String suggestion : suggestionSpan.getSuggestions()) { + if (!suggestion.equals(committedWordString)) { + suggestions.add(suggestion); + } + } + } else { + // If this is not a suggestion span, we just add it as is. + textToCommit.setSpan(span, 0 /* start */, lastCharIndex /* end */, + committedWordWithSuggestionSpans.getSpanFlags(span)); + } + } + // Add the suggestion list to the list of suggestions. + textToCommit.setSpan(new SuggestionSpan(inputTransaction.mSettingsValues.mLocale, + suggestions.toArray(new String[suggestions.size()]), 0 /* flags */), + 0 /* start */, lastCharIndex /* end */, 0 /* flags */); + } + if (inputTransaction.mSettingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces) { + // For languages with spaces, we revert to the typed string, but the cursor is still + // after the separator so we don't resume suggestions. If the user wants to correct + // the word, they have to press backspace again. + mConnection.commitText(textToCommit, 1); + } else { + // For languages without spaces, we revert the typed string but the cursor is flush + // with the typed word, so we need to resume suggestions right away. + final int[] codePoints = StringUtils.toCodePointArray(stringToCommit); + mWordComposer.setComposingWord(codePoints, + mLatinIME.getCoordinatesForCurrentKeyboard(codePoints)); + mConnection.setComposingText(textToCommit, 1); + } + // Don't restart suggestion yet. We'll restart if the user deletes the separator. + mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; + // We have a separator between the word and the cursor: we should show predictions. + inputTransaction.setRequiresUpdateSuggestions(); + } + + /** + * Factor in auto-caps and manual caps and compute the current caps mode. + * @param settingsValues the current settings values. + * @param keyboardShiftMode the current shift mode of the keyboard. See + * KeyboardSwitcher#getKeyboardShiftMode() for possible values. + * @return the actual caps mode the keyboard is in right now. + */ + private int getActualCapsMode(final SettingsValues settingsValues, + final int keyboardShiftMode) { + if (keyboardShiftMode != WordComposer.CAPS_MODE_AUTO_SHIFTED) { + return keyboardShiftMode; + } + final int auto = getCurrentAutoCapsState(settingsValues); + if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) { + return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED; + } + if (0 != auto) { + return WordComposer.CAPS_MODE_AUTO_SHIFTED; + } + return WordComposer.CAPS_MODE_OFF; + } + + /** + * Gets the current auto-caps state, factoring in the space state. + * + * This method tries its best to do this in the most efficient possible manner. It avoids + * getting text from the editor if possible at all. + * This is called from the KeyboardSwitcher (through a trampoline in LatinIME) because it + * needs to know auto caps state to display the right layout. + * + * @param settingsValues the relevant settings values + * @return a caps mode from TextUtils.CAP_MODE_* or Constants.TextUtils.CAP_MODE_OFF. + */ + public int getCurrentAutoCapsState(final SettingsValues settingsValues) { + if (!settingsValues.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF; + + final EditorInfo ei = getCurrentInputEditorInfo(); + if (ei == null) return Constants.TextUtils.CAP_MODE_OFF; + final int inputType = ei.inputType; + // Warning: this depends on mSpaceState, which may not be the most current value. If + // mSpaceState gets updated later, whoever called this may need to be told about it. + return mConnection.getCursorCapsMode(inputType, settingsValues.mSpacingAndPunctuations, + SpaceState.PHANTOM == mSpaceState); + } + + public int getCurrentRecapitalizeState() { + if (!mRecapitalizeStatus.isStarted() + || !mRecapitalizeStatus.isSetAt(mConnection.getExpectedSelectionStart(), + mConnection.getExpectedSelectionEnd())) { + // Not recapitalizing at the moment + return RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE; + } + return mRecapitalizeStatus.getCurrentMode(); + } + + /** + * @return the editor info for the current editor + */ + private EditorInfo getCurrentInputEditorInfo() { + return mLatinIME.getCurrentInputEditorInfo(); + } + + /** + * Get information fo previous words from the nth previous word before the cursor as context + * for the suggestion process. + * @param spacingAndPunctuations the current spacing and punctuations settings. + * @param nthPreviousWord reverse index of the word to get (1-indexed) + * @return the information of previous words + */ + // TODO: Make this private + public PrevWordsInfo getPrevWordsInfoFromNthPreviousWordForSuggestion( + final SpacingAndPunctuations spacingAndPunctuations, final int nthPreviousWord) { + if (spacingAndPunctuations.mCurrentLanguageHasSpaces) { + // If we are typing in a language with spaces we can just look up the previous + // word information from textview. + return mConnection.getPrevWordsInfoFromNthPreviousWord( + spacingAndPunctuations, nthPreviousWord); + } else { + return LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? + PrevWordsInfo.BEGINNING_OF_SENTENCE : + new PrevWordsInfo(new PrevWordsInfo.WordInfo( + mLastComposedWord.mCommittedWord.toString())); + } + } + + /** + * Tests the passed word for resumability. + * + * We can resume suggestions on words whose first code point is a word code point (with some + * nuances: check the code for details). + * + * @param settings the current values of the settings. + * @param word the word to evaluate. + * @return whether it's fine to resume suggestions on this word. + */ + private static boolean isResumableWord(final SettingsValues settings, final String word) { + final int firstCodePoint = word.codePointAt(0); + return settings.isWordCodePoint(firstCodePoint) + && Constants.CODE_SINGLE_QUOTE != firstCodePoint + && Constants.CODE_DASH != firstCodePoint; + } + + /** + * @param actionId the action to perform + */ + private void performEditorAction(final int actionId) { + mConnection.performEditorAction(actionId); + } + + /** + * Perform the processing specific to inputting TLDs. + * + * Some keys input a TLD (specifically, the ".com" key) and this warrants some specific + * processing. First, if this is a TLD, we ignore PHANTOM spaces -- this is done by type + * of character in onCodeInput, but since this gets inputted as a whole string we need to + * do it here specifically. Then, if the last character before the cursor is a period, then + * we cut the dot at the start of ".com". This is because humans tend to type "www.google." + * and then press the ".com" key and instinctively don't expect to get "www.google..com". + * + * @param text the raw text supplied to onTextInput + * @return the text to actually send to the editor + */ + private String performSpecificTldProcessingOnTextInput(final String text) { + if (text.length() <= 1 || text.charAt(0) != Constants.CODE_PERIOD + || !Character.isLetter(text.charAt(1))) { + // Not a tld: do nothing. + return text; + } + // We have a TLD (or something that looks like this): make sure we don't add + // a space even if currently in phantom mode. + mSpaceState = SpaceState.NONE; + final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); + // If no code point, #getCodePointBeforeCursor returns NOT_A_CODE_POINT. + if (Constants.CODE_PERIOD == codePointBeforeCursor) { + return text.substring(1); + } else { + return text; + } + } + + /** + * Handle a press on the settings key. + */ + private void onSettingsKeyPressed() { + mLatinIME.displaySettingsDialog(); + } + + /** + * Resets the whole input state to the starting state. + * + * This will clear the composing word, reset the last composed word, clear the suggestion + * strip and tell the input connection about it so that it can refresh its caches. + * + * @param newSelStart the new selection start, in java characters. + * @param newSelEnd the new selection end, in java characters. + * @param clearSuggestionStrip whether this method should clear the suggestion strip. + */ + // TODO: how is this different from startInput ?! + private void resetEntireInputState(final int newSelStart, final int newSelEnd, + final boolean clearSuggestionStrip) { + final boolean shouldFinishComposition = mWordComposer.isComposingWord(); + resetComposingState(true /* alsoResetLastComposedWord */); + if (clearSuggestionStrip) { + mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); + } + mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart, newSelEnd, + shouldFinishComposition); + } + + /** + * Resets only the composing state. + * + * Compare #resetEntireInputState, which also clears the suggestion strip and resets the + * input connection caches. This only deals with the composing state. + * + * @param alsoResetLastComposedWord whether to also reset the last composed word. + */ + private void resetComposingState(final boolean alsoResetLastComposedWord) { + mWordComposer.reset(); + if (alsoResetLastComposedWord) { + mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; + } + } + + /** + * Make a {@link com.android.inputmethod.latin.SuggestedWords} object containing a typed word + * and obsolete suggestions. + * See {@link com.android.inputmethod.latin.SuggestedWords#getTypedWordAndPreviousSuggestions( + * String, com.android.inputmethod.latin.SuggestedWords)}. + * @param typedWord The typed word as a string. + * @param previousSuggestedWords The previously suggested words. + * @return Obsolete suggestions with the newly typed word. + */ + private SuggestedWords retrieveOlderSuggestions(final String typedWord, + final SuggestedWords previousSuggestedWords) { + final SuggestedWords oldSuggestedWords = + previousSuggestedWords.isPunctuationSuggestions() ? SuggestedWords.EMPTY + : previousSuggestedWords; + final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions = + SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord, oldSuggestedWords); + return new SuggestedWords(typedWordAndPreviousSuggestions, null /* rawSuggestions */, + false /* typedWordValid */, false /* hasAutoCorrectionCandidate */, + true /* isObsoleteSuggestions */, false /* isPrediction */); + } + + /** + * Gets a chunk of text with or the auto-correction indicator underline span as appropriate. + * + * This method looks at the old state of the auto-correction indicator to put or not put + * the underline span as appropriate. It is important to note that this does not correspond + * exactly to whether this word will be auto-corrected to or not: what's important here is + * to keep the same indication as before. + * When we add a new code point to a composing word, we don't know yet if we are going to + * auto-correct it until the suggestions are computed. But in the mean time, we still need + * to display the character and to extend the previous underline. To avoid any flickering, + * the underline should keep the same color it used to have, even if that's not ultimately + * the correct color for this new word. When the suggestions are finished evaluating, we + * will call this method again to fix the color of the underline. + * + * @param text the text on which to maybe apply the span. + * @return the same text, with the auto-correction underline span if that's appropriate. + */ + // TODO: Shouldn't this go in some *Utils class instead? + private CharSequence getTextWithUnderline(final String text) { + return mIsAutoCorrectionIndicatorOn + ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(mLatinIME, text) + : text; + } + + /** + * Sends a DOWN key event followed by an UP key event to the editor. + * + * If possible at all, avoid using this method. It causes all sorts of race conditions with + * the text view because it goes through a different, asynchronous binder. Also, batch edits + * are ignored for key events. Use the normal software input methods instead. + * + * @param keyCode the key code to send inside the key event. + */ + private void sendDownUpKeyEvent(final int keyCode) { + final long eventTime = SystemClock.uptimeMillis(); + mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime, + KeyEvent.ACTION_DOWN, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, + KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); + mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime, + KeyEvent.ACTION_UP, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, + KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); + } + + /** + * Sends a code point to the editor, using the most appropriate method. + * + * Normally we send code points with commitText, but there are some cases (where backward + * compatibility is a concern for example) where we want to use deprecated methods. + * + * @param settingsValues the current values of the settings. + * @param codePoint the code point to send. + */ + // TODO: replace these two parameters with an InputTransaction + private void sendKeyCodePoint(final SettingsValues settingsValues, final int codePoint) { + // TODO: Remove this special handling of digit letters. + // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}. + if (codePoint >= '0' && codePoint <= '9') { + sendDownUpKeyEvent(codePoint - '0' + KeyEvent.KEYCODE_0); + return; + } + + // TODO: we should do this also when the editor has TYPE_NULL + if (Constants.CODE_ENTER == codePoint && settingsValues.isBeforeJellyBean()) { + // Backward compatibility mode. Before Jelly bean, the keyboard would simulate + // a hardware keyboard event on pressing enter or delete. This is bad for many + // reasons (there are race conditions with commits) but some applications are + // relying on this behavior so we continue to support it for older apps. + sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER); + } else { + mConnection.commitText(StringUtils.newSingleCodePointString(codePoint), 1); + } + } + + /** + * Promote a phantom space to an actual space. + * + * This essentially inserts a space, and that's it. It just checks the options and the text + * before the cursor are appropriate before doing it. + * + * @param settingsValues the current values of the settings. + */ + private void promotePhantomSpace(final SettingsValues settingsValues) { + if (settingsValues.shouldInsertSpacesAutomatically() + && settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces + && !mConnection.textBeforeCursorLooksLikeURL()) { + sendKeyCodePoint(settingsValues, Constants.CODE_SPACE); + } + } + + /** + * Do the final processing after a batch input has ended. This commits the word to the editor. + * @param settingsValues the current values of the settings. + * @param suggestedWords suggestedWords to use. + */ + public void onUpdateTailBatchInputCompleted(final SettingsValues settingsValues, + final SuggestedWords suggestedWords, + // TODO: remove this argument + final KeyboardSwitcher keyboardSwitcher) { + final String batchInputText = suggestedWords.isEmpty() ? null : suggestedWords.getWord(0); + if (TextUtils.isEmpty(batchInputText)) { + return; + } + mConnection.beginBatchEdit(); + if (SpaceState.PHANTOM == mSpaceState) { + promotePhantomSpace(settingsValues); + } + final SuggestedWordInfo autoCommitCandidate = mSuggestedWords.getAutoCommitCandidate(); + // Commit except the last word for phrase gesture if the top suggestion is eligible for auto + // commit. + if (settingsValues.mPhraseGestureEnabled && null != autoCommitCandidate) { + // Find the last space + final int indexOfLastSpace = batchInputText.lastIndexOf(Constants.CODE_SPACE) + 1; + if (0 != indexOfLastSpace) { + mConnection.commitText(batchInputText.substring(0, indexOfLastSpace), 1); + final SuggestedWords suggestedWordsForLastWordOfPhraseGesture = + suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture(); + mLatinIME.showSuggestionStrip(suggestedWordsForLastWordOfPhraseGesture); + } + final String lastWord = batchInputText.substring(indexOfLastSpace); + mWordComposer.setBatchInputWord(lastWord); + mConnection.setComposingText(lastWord, 1); + } else { + mWordComposer.setBatchInputWord(batchInputText); + mConnection.setComposingText(batchInputText, 1); + } + mConnection.endBatchEdit(); + // Space state must be updated before calling updateShiftState + mSpaceState = SpaceState.PHANTOM; + keyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(settingsValues), + getCurrentRecapitalizeState()); + } + + /** + * Commit the typed string to the editor. + * + * This is typically called when we should commit the currently composing word without applying + * auto-correction to it. Typically, we come here upon pressing a separator when the keyboard + * is configured to not do auto-correction at all (because of the settings or the properties of + * the editor). In this case, `separatorString' is set to the separator that was pressed. + * We also come here in a variety of cases with external user action. For example, when the + * cursor is moved while there is a composition, or when the keyboard is closed, or when the + * user presses the Send button for an SMS, we don't auto-correct as that would be unexpected. + * In this case, `separatorString' is set to NOT_A_SEPARATOR. + * + * @param settingsValues the current values of the settings. + * @param separatorString the separator that's causing the commit, or NOT_A_SEPARATOR if none. + */ + // TODO: Make this private + public void commitTyped(final SettingsValues settingsValues, final String separatorString) { + if (!mWordComposer.isComposingWord()) return; + final String typedWord = mWordComposer.getTypedWord(); + if (typedWord.length() > 0) { + commitChosenWord(settingsValues, typedWord, + LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, separatorString); + } + } + + /** + * Commit the current auto-correction. + * + * This will commit the best guess of the keyboard regarding what the user meant by typing + * the currently composing word. The IME computes suggestions and assigns a confidence score + * to each of them; when it's confident enough in one suggestion, it replaces the typed string + * by this suggestion at commit time. When it's not confident enough, or when it has no + * suggestions, or when the settings or environment does not allow for auto-correction, then + * this method just commits the typed string. + * Note that if suggestions are currently being computed in the background, this method will + * block until the computation returns. This is necessary for consistency (it would be very + * strange if pressing space would commit a different word depending on how fast you press). + * + * @param settingsValues the current value of the settings. + * @param separator the separator that's causing the commit to happen. + */ + private void commitCurrentAutoCorrection(final SettingsValues settingsValues, + final String separator, + // TODO: Remove this argument. + final LatinIME.UIHandler handler) { + // Complete any pending suggestions query first + if (handler.hasPendingUpdateSuggestions()) { + handler.cancelUpdateSuggestionStrip(); + performUpdateSuggestionStripSync(settingsValues); + } + final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull(); + final String typedWord = mWordComposer.getTypedWord(); + final String autoCorrection = (typedAutoCorrection != null) + ? typedAutoCorrection : typedWord; + if (autoCorrection != null) { + if (TextUtils.isEmpty(typedWord)) { + throw new RuntimeException("We have an auto-correction but the typed word " + + "is empty? Impossible! I must commit suicide."); + } + commitChosenWord(settingsValues, autoCorrection, + LastComposedWord.COMMIT_TYPE_DECIDED_WORD, separator); + if (!typedWord.equals(autoCorrection)) { + // This will make the correction flash for a short while as a visual clue + // to the user that auto-correction happened. It has no other effect; in particular + // note that this won't affect the text inside the text field AT ALL: it only makes + // the segment of text starting at the supplied index and running for the length + // of the auto-correction flash. At this moment, the "typedWord" argument is + // ignored by TextView. + mConnection.commitCorrection(new CorrectionInfo( + mConnection.getExpectedSelectionEnd() - autoCorrection.length(), + typedWord, autoCorrection)); + } + } + } + + /** + * Commits the chosen word to the text field and saves it for later retrieval. + * + * @param settingsValues the current values of the settings. + * @param chosenWord the word we want to commit. + * @param commitType the type of the commit, as one of LastComposedWord.COMMIT_TYPE_* + * @param separatorString the separator that's causing the commit, or NOT_A_SEPARATOR if none. + */ + private void commitChosenWord(final SettingsValues settingsValues, final String chosenWord, + final int commitType, final String separatorString) { + final SuggestedWords suggestedWords = mSuggestedWords; + final CharSequence chosenWordWithSuggestions = + SuggestionSpanUtils.getTextWithSuggestionSpan(mLatinIME, chosenWord, + suggestedWords); + // When we are composing word, get previous words information from the 2nd previous word + // because the 1st previous word is the word to be committed. Otherwise get previous words + // information from the 1st previous word. + final PrevWordsInfo prevWordsInfo = mConnection.getPrevWordsInfoFromNthPreviousWord( + settingsValues.mSpacingAndPunctuations, mWordComposer.isComposingWord() ? 2 : 1); + mConnection.commitText(chosenWordWithSuggestions, 1); + // Add the word to the user history dictionary + performAdditionToUserHistoryDictionary(settingsValues, chosenWord, prevWordsInfo); + // TODO: figure out here if this is an auto-correct or if the best word is actually + // what user typed. Note: currently this is done much later in + // LastComposedWord#didCommitTypedWord by string equality of the remembered + // strings. + mLastComposedWord = mWordComposer.commitWord(commitType, + chosenWordWithSuggestions, separatorString, prevWordsInfo); + } + + /** + * Retry resetting caches in the rich input connection. + * + * When the editor can't be accessed we can't reset the caches, so we schedule a retry. + * This method handles the retry, and re-schedules a new retry if we still can't access. + * We only retry up to 5 times before giving up. + * + * @param settingsValues the current values of the settings. + * @param tryResumeSuggestions Whether we should resume suggestions or not. + * @param remainingTries How many times we may try again before giving up. + * @return whether true if the caches were successfully reset, false otherwise. + */ + // TODO: make this private + public boolean retryResetCachesAndReturnSuccess(final SettingsValues settingsValues, + final boolean tryResumeSuggestions, final int remainingTries, + // TODO: remove these arguments + final LatinIME.UIHandler handler) { + final boolean shouldFinishComposition = mConnection.hasSelection() + || !mConnection.isCursorPositionKnown(); + if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess( + mConnection.getExpectedSelectionStart(), mConnection.getExpectedSelectionEnd(), + shouldFinishComposition)) { + if (0 < remainingTries) { + handler.postResetCaches(tryResumeSuggestions, remainingTries - 1); + return false; + } + // If remainingTries is 0, we should stop waiting for new tries, however we'll still + // return true as we need to perform other tasks (for example, loading the keyboard). + } + mConnection.tryFixLyingCursorPosition(); + if (tryResumeSuggestions) { + // This is triggered when starting input anew, so we want to include the resumed + // word in suggestions. + handler.postResumeSuggestions(true /* shouldIncludeResumedWordInSuggestions */, + true /* shouldDelay */); + } + return true; + } + + public void getSuggestedWords(final SettingsValues settingsValues, + final ProximityInfo proximityInfo, final int keyboardShiftMode, final int sessionId, + final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { + mWordComposer.adviseCapitalizedModeBeforeFetchingSuggestions( + getActualCapsMode(settingsValues, keyboardShiftMode)); + mSuggest.getSuggestedWords(mWordComposer, + getPrevWordsInfoFromNthPreviousWordForSuggestion( + settingsValues.mSpacingAndPunctuations, + // Get the word on which we should search the bigrams. If we are composing + // a word, it's whatever is *before* the half-committed word in the buffer, + // hence 2; if we aren't, we should just skip whitespace if any, so 1. + mWordComposer.isComposingWord() ? 2 : 1), + proximityInfo, settingsValues.mBlockPotentiallyOffensive, + settingsValues.mAutoCorrectionEnabled, + settingsValues.mAdditionalFeaturesSettingValues, + sessionId, sequenceNumber, callback); + } +} diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java new file mode 100644 index 000000000..9dbe2c38b --- /dev/null +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.inputlogic; + +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; + +import com.android.inputmethod.compat.LooperCompatUtils; +import com.android.inputmethod.latin.InputPointers; +import com.android.inputmethod.latin.LatinIME; +import com.android.inputmethod.latin.Suggest; +import com.android.inputmethod.latin.SuggestedWords; +import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback; + +/** + * A helper to manage deferred tasks for the input logic. + */ +class InputLogicHandler implements Handler.Callback { + final Handler mNonUIThreadHandler; + // TODO: remove this reference. + final LatinIME mLatinIME; + final InputLogic mInputLogic; + private final Object mLock = new Object(); + private boolean mInBatchInput; // synchronized using {@link #mLock}. + + private static final int MSG_GET_SUGGESTED_WORDS = 1; + + // A handler that never does anything. This is used for cases where events come before anything + // is initialized, though probably only the monkey can actually do this. + public static final InputLogicHandler NULL_HANDLER = new InputLogicHandler() { + @Override + public void reset() {} + @Override + public boolean handleMessage(final Message msg) { return true; } + @Override + public void onStartBatchInput() {} + @Override + public void onUpdateBatchInput(final InputPointers batchPointers, + final int sequenceNumber) {} + @Override + public void onCancelBatchInput() {} + @Override + public void updateTailBatchInput(final InputPointers batchPointers, + final int sequenceNumber) {} + @Override + public void getSuggestedWords(final int sessionId, final int sequenceNumber, + final OnGetSuggestedWordsCallback callback) {} + }; + + private InputLogicHandler() { + mNonUIThreadHandler = null; + mLatinIME = null; + mInputLogic = null; + } + + public InputLogicHandler(final LatinIME latinIME, final InputLogic inputLogic) { + final HandlerThread handlerThread = new HandlerThread( + InputLogicHandler.class.getSimpleName()); + handlerThread.start(); + mNonUIThreadHandler = new Handler(handlerThread.getLooper(), this); + mLatinIME = latinIME; + mInputLogic = inputLogic; + } + + public void reset() { + mNonUIThreadHandler.removeCallbacksAndMessages(null); + } + + // In unit tests, we create several instances of LatinIME, which results in several instances + // of InputLogicHandler. To avoid these handlers lingering, we call this. + public void destroy() { + LooperCompatUtils.quitSafely(mNonUIThreadHandler.getLooper()); + } + + /** + * Handle a message. + * @see android.os.Handler.Callback#handleMessage(android.os.Message) + */ + // Called on the Non-UI handler thread by the Handler code. + @Override + public boolean handleMessage(final Message msg) { + switch (msg.what) { + case MSG_GET_SUGGESTED_WORDS: + mLatinIME.getSuggestedWords(msg.arg1 /* sessionId */, + msg.arg2 /* sequenceNumber */, (OnGetSuggestedWordsCallback) msg.obj); + break; + } + return true; + } + + // Called on the UI thread by InputLogic. + public void onStartBatchInput() { + synchronized (mLock) { + mInBatchInput = true; + } + } + + public boolean isInBatchInput() { + return mInBatchInput; + } + + /** + * Fetch suggestions corresponding to an update of a batch input. + * @param batchPointers the updated pointers, including the part that was passed last time. + * @param sequenceNumber the sequence number associated with this batch input. + * @param isTailBatchInput true if this is the end of a batch input, false if it's an update. + */ + // This method can be called from any thread and will see to it that the correct threads + // are used for parts that require it. This method will send a message to the Non-UI handler + // thread to pull suggestions, and get the inlined callback to get called on the Non-UI + // handler thread. If this is the end of a batch input, the callback will then proceed to + // send a message to the UI handler in LatinIME so that showing suggestions can be done on + // the UI thread. + private void updateBatchInput(final InputPointers batchPointers, + final int sequenceNumber, final boolean isTailBatchInput) { + synchronized (mLock) { + if (!mInBatchInput) { + // Batch input has ended or canceled while the message was being delivered. + return; + } + mInputLogic.mWordComposer.setBatchInputPointers(batchPointers); + getSuggestedWords(Suggest.SESSION_GESTURE, sequenceNumber, + new OnGetSuggestedWordsCallback() { + @Override + public void onGetSuggestedWords(SuggestedWords suggestedWords) { + // We're now inside the callback. This always runs on the Non-UI thread, + // no matter what thread updateBatchInput was originally called on. + if (suggestedWords.isEmpty()) { + // Use old suggestions if we don't have any new ones. + // Previous suggestions are found in InputLogic#mSuggestedWords. + // Since these are the most recent ones and we just recomputed + // new ones to update them, then the previous ones are there. + suggestedWords = mInputLogic.mSuggestedWords; + } + mLatinIME.mHandler.showGesturePreviewAndSuggestionStrip(suggestedWords, + isTailBatchInput /* dismissGestureFloatingPreviewText */); + if (isTailBatchInput) { + mInBatchInput = false; + // The following call schedules onEndBatchInputInternal + // to be called on the UI thread. + mLatinIME.mHandler.showTailBatchInputResult(suggestedWords); + } + } + }); + } + } + + /** + * Update a batch input. + * + * This fetches suggestions and updates the suggestion strip and the floating text preview. + * + * @param batchPointers the updated batch pointers. + * @param sequenceNumber the sequence number associated with this batch input. + */ + // Called on the UI thread by InputLogic. + public void onUpdateBatchInput(final InputPointers batchPointers, + final int sequenceNumber) { + updateBatchInput(batchPointers, sequenceNumber, false /* isTailBatchInput */); + } + + /** + * Cancel a batch input. + * + * Note that as opposed to updateTailBatchInput, we do the UI side of this immediately on the + * same thread, rather than get this to call a method in LatinIME. This is because + * canceling a batch input does not necessitate the long operation of pulling suggestions. + */ + // Called on the UI thread by InputLogic. + public void onCancelBatchInput() { + synchronized (mLock) { + mInBatchInput = false; + } + } + + /** + * Trigger an update for a tail batch input. + * + * A tail batch input is the last update for a gesture, the one that is triggered after the + * user lifts their finger. This method schedules fetching suggestions on the non-UI thread, + * then when the suggestions are computed it comes back on the UI thread to update the + * suggestion strip, commit the first suggestion, and dismiss the floating text preview. + * + * @param batchPointers the updated batch pointers. + * @param sequenceNumber the sequence number associated with this batch input. + */ + // Called on the UI thread by InputLogic. + public void updateTailBatchInput(final InputPointers batchPointers, + final int sequenceNumber) { + updateBatchInput(batchPointers, sequenceNumber, true /* isTailBatchInput */); + } + + public void getSuggestedWords(final int sessionId, final int sequenceNumber, + final OnGetSuggestedWordsCallback callback) { + mNonUIThreadHandler.obtainMessage( + MSG_GET_SUGGESTED_WORDS, sessionId, sequenceNumber, callback).sendToTarget(); + } +} diff --git a/java/src/com/android/inputmethod/latin/inputlogic/SpaceState.java b/java/src/com/android/inputmethod/latin/inputlogic/SpaceState.java new file mode 100644 index 000000000..ce80c0016 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/inputlogic/SpaceState.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.inputlogic; + +/** + * Class for managing space states. + * + * At any given time, the input logic is in one of five possible space states. Depending on the + * current space state, some behavior will change; the prime example of this is the PHANTOM state, + * in which any subsequent letter input will input a space before the letter. Read on the + * description inside this class for each of the space states. + */ +public class SpaceState { + // None: the state where all the keyboard behavior is the most "standard" and no automatic + // input is added or removed. In this state, all self-inserting keys only insert themselves, + // and backspace removes one character. + public static final int NONE = 0; + // Double space: the state where the user pressed space twice quickly, which LatinIME + // resolved as period-space. In this state, pressing backspace will undo the + // double-space-to-period insertion: it will replace ". " with " ". + public static final int DOUBLE = 1; + // Swap punctuation: the state where a weak space and a punctuation from the suggestion strip + // have just been swapped. In this state, pressing backspace will undo the swap: the + // characters will be swapped back back, and the space state will go to WEAK. + public static final int SWAP_PUNCTUATION = 2; + // Weak space: a space that should be swapped only by suggestion strip punctuation. Weak + // spaces happen when the user presses space, accepting the current suggestion (whether + // it's an auto-correction or not). In this state, pressing a punctuation from the suggestion + // strip inserts it before the space (while it inserts it after the space in the NONE state). + public static final int WEAK = 3; + // Phantom space: a not-yet-inserted space that should get inserted on the next input, + // character provided it's not a separator. If it's a separator, the phantom space is dropped. + // Phantom spaces happen when a user chooses a word from the suggestion strip. In this state, + // non-separators insert a space before they get inserted. + public static final int PHANTOM = 4; + + private SpaceState() { + // This class is not publicly instantiable. + } +} diff --git a/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java deleted file mode 100644 index fda97dafc..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding; -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; -import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.TreeMap; - -/** - * A base class of the binary dictionary decoder. - */ -public abstract class AbstractDictDecoder implements DictDecoder { - protected FileHeader readHeader(final DictBuffer dictBuffer) - throws IOException, UnsupportedFormatException { - if (dictBuffer == null) { - openDictBuffer(); - } - - final int version = HeaderReader.readVersion(dictBuffer); - if (version < FormatSpec.MINIMUM_SUPPORTED_VERSION - || version > FormatSpec.MAXIMUM_SUPPORTED_VERSION) { - throw new UnsupportedFormatException("Unsupported version : " + version); - } - // TODO: Remove this field. - final int optionsFlags = HeaderReader.readOptionFlags(dictBuffer); - - final int headerSize = HeaderReader.readHeaderSize(dictBuffer); - - if (headerSize < 0) { - throw new UnsupportedFormatException("header size can't be negative."); - } - - final HashMap<String, String> attributes = HeaderReader.readAttributes(dictBuffer, - headerSize); - - final FileHeader header = new FileHeader(headerSize, - new FusionDictionary.DictionaryOptions(attributes, - 0 != (optionsFlags & FormatSpec.GERMAN_UMLAUT_PROCESSING_FLAG), - 0 != (optionsFlags & FormatSpec.FRENCH_LIGATURE_PROCESSING_FLAG)), - new FormatOptions(version, - 0 != (optionsFlags & FormatSpec.SUPPORTS_DYNAMIC_UPDATE), - 0 != (optionsFlags & FormatSpec.CONTAINS_TIMESTAMP_FLAG))); - return header; - } - - @Override @UsedForTesting - public int getTerminalPosition(final String word) - throws IOException, UnsupportedFormatException { - if (!isDictBufferOpen()) { - openDictBuffer(); - } - return BinaryDictIOUtils.getTerminalPosition(this, word); - } - - @Override @UsedForTesting - public void readUnigramsAndBigramsBinary(final TreeMap<Integer, String> words, - final TreeMap<Integer, Integer> frequencies, - final TreeMap<Integer, ArrayList<PendingAttribute>> bigrams) - throws IOException, UnsupportedFormatException { - if (!isDictBufferOpen()) { - openDictBuffer(); - } - BinaryDictIOUtils.readUnigramsAndBigramsBinary(this, words, frequencies, bigrams); - } - - /** - * A utility class for reading a file header. - */ - protected static class HeaderReader { - protected static int readVersion(final DictBuffer dictBuffer) - throws IOException, UnsupportedFormatException { - return BinaryDictDecoderUtils.checkFormatVersion(dictBuffer); - } - - protected static int readOptionFlags(final DictBuffer dictBuffer) { - return dictBuffer.readUnsignedShort(); - } - - protected static int readHeaderSize(final DictBuffer dictBuffer) { - return dictBuffer.readInt(); - } - - protected static HashMap<String, String> readAttributes(final DictBuffer dictBuffer, - final int headerSize) { - final HashMap<String, String> attributes = new HashMap<String, String>(); - while (dictBuffer.position() < headerSize) { - // We can avoid an infinite loop here since dictBuffer.position() is always - // increased by calling CharEncoding.readString. - final String key = CharEncoding.readString(dictBuffer); - final String value = CharEncoding.readString(dictBuffer); - attributes.put(key, value); - } - dictBuffer.position(headerSize); - return attributes; - } - } - - /** - * A utility class for reading a PtNode. - */ - protected static class PtNodeReader { - protected static int readPtNodeOptionFlags(final DictBuffer dictBuffer) { - return dictBuffer.readUnsignedByte(); - } - - protected static int readParentAddress(final DictBuffer dictBuffer, - final FormatOptions formatOptions) { - if (BinaryDictIOUtils.supportsDynamicUpdate(formatOptions)) { - return BinaryDictDecoderUtils.readSInt24(dictBuffer); - } else { - return FormatSpec.NO_PARENT_ADDRESS; - } - } - - protected static int readChildrenAddress(final DictBuffer dictBuffer, final int optionFlags, - final FormatOptions formatOptions) { - if (BinaryDictIOUtils.supportsDynamicUpdate(formatOptions)) { - final int address = BinaryDictDecoderUtils.readSInt24(dictBuffer); - if (address == 0) return FormatSpec.NO_CHILDREN_ADDRESS; - return address; - } else { - switch (optionFlags & FormatSpec.MASK_CHILDREN_ADDRESS_TYPE) { - case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE: - return dictBuffer.readUnsignedByte(); - case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES: - return dictBuffer.readUnsignedShort(); - case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES: - return dictBuffer.readUnsignedInt24(); - case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS: - default: - return FormatSpec.NO_CHILDREN_ADDRESS; - } - } - } - - // Reads shortcuts and returns the read length. - protected static int readShortcut(final DictBuffer dictBuffer, - final ArrayList<WeightedString> shortcutTargets) { - final int pointerBefore = dictBuffer.position(); - dictBuffer.readUnsignedShort(); // skip the size - while (true) { - final int targetFlags = dictBuffer.readUnsignedByte(); - final String word = CharEncoding.readString(dictBuffer); - shortcutTargets.add(new WeightedString(word, - targetFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY)); - if (0 == (targetFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT)) break; - } - return dictBuffer.position() - pointerBefore; - } - - protected static int readBigramAddresses(final DictBuffer dictBuffer, - final ArrayList<PendingAttribute> bigrams, final int baseAddress) { - int readLength = 0; - int bigramCount = 0; - while (bigramCount++ < FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) { - final int bigramFlags = dictBuffer.readUnsignedByte(); - ++readLength; - final int sign = 0 == (bigramFlags & FormatSpec.FLAG_BIGRAM_ATTR_OFFSET_NEGATIVE) - ? 1 : -1; - int bigramAddress = baseAddress + readLength; - switch (bigramFlags & FormatSpec.MASK_BIGRAM_ATTR_ADDRESS_TYPE) { - case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE: - bigramAddress += sign * dictBuffer.readUnsignedByte(); - readLength += 1; - break; - case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES: - bigramAddress += sign * dictBuffer.readUnsignedShort(); - readLength += 2; - break; - case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES: - bigramAddress += sign * dictBuffer.readUnsignedInt24(); - readLength += 3; - break; - default: - throw new RuntimeException("Has bigrams with no address"); - } - bigrams.add(new PendingAttribute( - bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY, - bigramAddress)); - if (0 == (bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT)) break; - } - return readLength; - } - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java deleted file mode 100644 index 216492b4d..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java +++ /dev/null @@ -1,623 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; -import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.util.ArrayList; -import java.util.Map; -import java.util.TreeMap; - -/** - * Decodes binary files for a FusionDictionary. - * - * All the methods in this class are static. - * - * TODO: Remove calls from classes except Ver3DictDecoder - * TODO: Move this file to makedict/internal. - * TODO: Rename this class to DictDecoderUtils. - */ -public final class BinaryDictDecoderUtils { - - private static final boolean DBG = MakedictLog.DBG; - - private BinaryDictDecoderUtils() { - // This utility class is not publicly instantiable. - } - - private static final int MAX_JUMPS = 12; - - @UsedForTesting - public interface DictBuffer { - public int readUnsignedByte(); - public int readUnsignedShort(); - public int readUnsignedInt24(); - public int readInt(); - public int position(); - public void position(int newPosition); - public void put(final byte b); - public int limit(); - @UsedForTesting - public int capacity(); - } - - public static final class ByteBufferDictBuffer implements DictBuffer { - private ByteBuffer mBuffer; - - public ByteBufferDictBuffer(final ByteBuffer buffer) { - mBuffer = buffer; - } - - @Override - public int readUnsignedByte() { - return mBuffer.get() & 0xFF; - } - - @Override - public int readUnsignedShort() { - return mBuffer.getShort() & 0xFFFF; - } - - @Override - public int readUnsignedInt24() { - final int retval = readUnsignedByte(); - return (retval << 16) + readUnsignedShort(); - } - - @Override - public int readInt() { - return mBuffer.getInt(); - } - - @Override - public int position() { - return mBuffer.position(); - } - - @Override - public void position(int newPos) { - mBuffer.position(newPos); - } - - @Override - public void put(final byte b) { - mBuffer.put(b); - } - - @Override - public int limit() { - return mBuffer.limit(); - } - - @Override - public int capacity() { - return mBuffer.capacity(); - } - } - - /** - * A class grouping utility function for our specific character encoding. - */ - static final class CharEncoding { - private static final int MINIMAL_ONE_BYTE_CHARACTER_VALUE = 0x20; - private static final int MAXIMAL_ONE_BYTE_CHARACTER_VALUE = 0xFF; - - /** - * Helper method to find out whether this code fits on one byte - */ - private static boolean fitsOnOneByte(final int character) { - return character >= MINIMAL_ONE_BYTE_CHARACTER_VALUE - && character <= MAXIMAL_ONE_BYTE_CHARACTER_VALUE; - } - - /** - * Compute the size of a character given its character code. - * - * Char format is: - * 1 byte = bbbbbbbb match - * case 000xxxxx: xxxxx << 16 + next byte << 8 + next byte - * else: if 00011111 (= 0x1F) : this is the terminator. This is a relevant choice because - * unicode code points range from 0 to 0x10FFFF, so any 3-byte value starting with - * 00011111 would be outside unicode. - * else: iso-latin-1 code - * This allows for the whole unicode range to be encoded, including chars outside of - * the BMP. Also everything in the iso-latin-1 charset is only 1 byte, except control - * characters which should never happen anyway (and still work, but take 3 bytes). - * - * @param character the character code. - * @return the size in binary encoded-form, either 1 or 3 bytes. - */ - static int getCharSize(final int character) { - // See char encoding in FusionDictionary.java - if (fitsOnOneByte(character)) return 1; - if (FormatSpec.INVALID_CHARACTER == character) return 1; - return 3; - } - - /** - * Compute the byte size of a character array. - */ - static int getCharArraySize(final int[] chars) { - int size = 0; - for (int character : chars) size += getCharSize(character); - return size; - } - - /** - * Writes a char array to a byte buffer. - * - * @param codePoints the code point array to write. - * @param buffer the byte buffer to write to. - * @param index the index in buffer to write the character array to. - * @return the index after the last character. - */ - static int writeCharArray(final int[] codePoints, final byte[] buffer, int index) { - for (int codePoint : codePoints) { - if (1 == getCharSize(codePoint)) { - buffer[index++] = (byte)codePoint; - } else { - buffer[index++] = (byte)(0xFF & (codePoint >> 16)); - buffer[index++] = (byte)(0xFF & (codePoint >> 8)); - buffer[index++] = (byte)(0xFF & codePoint); - } - } - return index; - } - - /** - * Writes a string with our character format to a byte buffer. - * - * This will also write the terminator byte. - * - * @param buffer the byte buffer to write to. - * @param origin the offset to write from. - * @param word the string to write. - * @return the size written, in bytes. - */ - static int writeString(final byte[] buffer, final int origin, - final String word) { - final int length = word.length(); - int index = origin; - for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) { - final int codePoint = word.codePointAt(i); - if (1 == getCharSize(codePoint)) { - buffer[index++] = (byte)codePoint; - } else { - buffer[index++] = (byte)(0xFF & (codePoint >> 16)); - buffer[index++] = (byte)(0xFF & (codePoint >> 8)); - buffer[index++] = (byte)(0xFF & codePoint); - } - } - buffer[index++] = FormatSpec.PTNODE_CHARACTERS_TERMINATOR; - return index - origin; - } - - /** - * Writes a string with our character format to an OutputStream. - * - * This will also write the terminator byte. - * - * @param buffer the OutputStream to write to. - * @param word the string to write. - */ - static void writeString(final OutputStream buffer, final String word) throws IOException { - final int length = word.length(); - for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) { - final int codePoint = word.codePointAt(i); - if (1 == getCharSize(codePoint)) { - buffer.write((byte) codePoint); - } else { - buffer.write((byte) (0xFF & (codePoint >> 16))); - buffer.write((byte) (0xFF & (codePoint >> 8))); - buffer.write((byte) (0xFF & codePoint)); - } - } - buffer.write(FormatSpec.PTNODE_CHARACTERS_TERMINATOR); - } - - /** - * Reads a string from a DictBuffer. This is the converse of the above method. - */ - static String readString(final DictBuffer dictBuffer) { - final StringBuilder s = new StringBuilder(); - int character = readChar(dictBuffer); - while (character != FormatSpec.INVALID_CHARACTER) { - s.appendCodePoint(character); - character = readChar(dictBuffer); - } - return s.toString(); - } - - /** - * Reads a character from the buffer. - * - * This follows the character format documented earlier in this source file. - * - * @param dictBuffer the buffer, positioned over an encoded character. - * @return the character code. - */ - static int readChar(final DictBuffer dictBuffer) { - int character = dictBuffer.readUnsignedByte(); - if (!fitsOnOneByte(character)) { - if (FormatSpec.PTNODE_CHARACTERS_TERMINATOR == character) { - return FormatSpec.INVALID_CHARACTER; - } - character <<= 16; - character += dictBuffer.readUnsignedShort(); - } - return character; - } - } - - // Input methods: Read a binary dictionary to memory. - // readDictionaryBinary is the public entry point for them. - - static int readSInt24(final DictBuffer dictBuffer) { - final int retval = dictBuffer.readUnsignedInt24(); - final int sign = ((retval & FormatSpec.MSB24) != 0) ? -1 : 1; - return sign * (retval & FormatSpec.SINT24_MAX); - } - - static int readChildrenAddress(final DictBuffer dictBuffer, - final int optionFlags, final FormatOptions options) { - if (options.mSupportsDynamicUpdate) { - final int address = dictBuffer.readUnsignedInt24(); - if (address == 0) return FormatSpec.NO_CHILDREN_ADDRESS; - if ((address & FormatSpec.MSB24) != 0) { - return -(address & FormatSpec.SINT24_MAX); - } else { - return address; - } - } - switch (optionFlags & FormatSpec.MASK_CHILDREN_ADDRESS_TYPE) { - case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE: - return dictBuffer.readUnsignedByte(); - case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES: - return dictBuffer.readUnsignedShort(); - case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES: - return dictBuffer.readUnsignedInt24(); - case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS: - default: - return FormatSpec.NO_CHILDREN_ADDRESS; - } - } - - static int readParentAddress(final DictBuffer dictBuffer, - final FormatOptions formatOptions) { - if (BinaryDictIOUtils.supportsDynamicUpdate(formatOptions)) { - final int parentAddress = dictBuffer.readUnsignedInt24(); - final int sign = ((parentAddress & FormatSpec.MSB24) != 0) ? -1 : 1; - return sign * (parentAddress & FormatSpec.SINT24_MAX); - } else { - return FormatSpec.NO_PARENT_ADDRESS; - } - } - - /** - * Reads and returns the PtNode count out of a buffer and forwards the pointer. - */ - /* package */ static int readPtNodeCount(final DictBuffer dictBuffer) { - final int msb = dictBuffer.readUnsignedByte(); - if (FormatSpec.MAX_PTNODES_FOR_ONE_BYTE_PTNODE_COUNT >= msb) { - return msb; - } else { - return ((FormatSpec.MAX_PTNODES_FOR_ONE_BYTE_PTNODE_COUNT & msb) << 8) - + dictBuffer.readUnsignedByte(); - } - } - - /** - * Finds, as a string, the word at the position passed as an argument. - * - * @param dictDecoder the dict decoder. - * @param headerSize the size of the header. - * @param pos the position to seek. - * @param formatOptions file format options. - * @return the word with its frequency, as a weighted string. - */ - /* package for tests */ static WeightedString getWordAtPosition(final DictDecoder dictDecoder, - final int headerSize, final int pos, final FormatOptions formatOptions) { - final WeightedString result; - final int originalPos = dictDecoder.getPosition(); - dictDecoder.setPosition(pos); - - if (BinaryDictIOUtils.supportsDynamicUpdate(formatOptions)) { - result = getWordAtPositionWithParentAddress(dictDecoder, pos, formatOptions); - } else { - result = getWordAtPositionWithoutParentAddress(dictDecoder, headerSize, pos, - formatOptions); - } - - dictDecoder.setPosition(originalPos); - return result; - } - - @SuppressWarnings("unused") - private static WeightedString getWordAtPositionWithParentAddress(final DictDecoder dictDecoder, - final int pos, final FormatOptions options) { - int currentPos = pos; - int frequency = Integer.MIN_VALUE; - final StringBuilder builder = new StringBuilder(); - // the length of the path from the root to the leaf is limited by MAX_WORD_LENGTH - for (int count = 0; count < FormatSpec.MAX_WORD_LENGTH; ++count) { - PtNodeInfo currentInfo; - int loopCounter = 0; - do { - dictDecoder.setPosition(currentPos); - currentInfo = dictDecoder.readPtNode(currentPos, options); - if (BinaryDictIOUtils.isMovedPtNode(currentInfo.mFlags, options)) { - currentPos = currentInfo.mParentAddress + currentInfo.mOriginalAddress; - } - if (DBG && loopCounter++ > MAX_JUMPS) { - MakedictLog.d("Too many jumps - probably a bug"); - } - } while (BinaryDictIOUtils.isMovedPtNode(currentInfo.mFlags, options)); - if (Integer.MIN_VALUE == frequency) frequency = currentInfo.mFrequency; - builder.insert(0, - new String(currentInfo.mCharacters, 0, currentInfo.mCharacters.length)); - if (currentInfo.mParentAddress == FormatSpec.NO_PARENT_ADDRESS) break; - currentPos = currentInfo.mParentAddress + currentInfo.mOriginalAddress; - } - return new WeightedString(builder.toString(), frequency); - } - - private static WeightedString getWordAtPositionWithoutParentAddress( - final DictDecoder dictDecoder, final int headerSize, final int pos, - final FormatOptions options) { - dictDecoder.setPosition(headerSize); - final int count = dictDecoder.readPtNodeCount(); - int groupPos = headerSize + BinaryDictIOUtils.getPtNodeCountSize(count); - final StringBuilder builder = new StringBuilder(); - WeightedString result = null; - - PtNodeInfo last = null; - for (int i = count - 1; i >= 0; --i) { - PtNodeInfo info = dictDecoder.readPtNode(groupPos, options); - groupPos = info.mEndAddress; - if (info.mOriginalAddress == pos) { - builder.append(new String(info.mCharacters, 0, info.mCharacters.length)); - result = new WeightedString(builder.toString(), info.mFrequency); - break; // and return - } - if (BinaryDictIOUtils.hasChildrenAddress(info.mChildrenAddress)) { - if (info.mChildrenAddress > pos) { - if (null == last) continue; - builder.append(new String(last.mCharacters, 0, last.mCharacters.length)); - dictDecoder.setPosition(last.mChildrenAddress); - i = dictDecoder.readPtNodeCount(); - groupPos = last.mChildrenAddress + BinaryDictIOUtils.getPtNodeCountSize(i); - last = null; - continue; - } - last = info; - } - if (0 == i && BinaryDictIOUtils.hasChildrenAddress(last.mChildrenAddress)) { - builder.append(new String(last.mCharacters, 0, last.mCharacters.length)); - dictDecoder.setPosition(last.mChildrenAddress); - i = dictDecoder.readPtNodeCount(); - groupPos = last.mChildrenAddress + BinaryDictIOUtils.getPtNodeCountSize(i); - last = null; - continue; - } - } - return result; - } - - /** - * Reads a single node array from a buffer. - * - * This methods reads the file at the current position. A node array is fully expected to start - * at the current position. - * This will recursively read other node arrays into the structure, populating the reverse - * maps on the fly and using them to keep track of already read nodes. - * - * @param dictDecoder the dict decoder, correctly positioned at the start of a node array. - * @param headerSize the size, in bytes, of the file header. - * @param reverseNodeArrayMap a mapping from addresses to already read node arrays. - * @param reversePtNodeMap a mapping from addresses to already read PtNodes. - * @param options file format options. - * @return the read node array with all his children already read. - */ - private static PtNodeArray readNodeArray(final DictDecoder dictDecoder, - final int headerSize, final Map<Integer, PtNodeArray> reverseNodeArrayMap, - final Map<Integer, PtNode> reversePtNodeMap, final FormatOptions options) - throws IOException { - final ArrayList<PtNode> nodeArrayContents = new ArrayList<PtNode>(); - final int nodeArrayOriginPos = dictDecoder.getPosition(); - - do { // Scan the linked-list node. - final int nodeArrayHeadPos = dictDecoder.getPosition(); - final int count = dictDecoder.readPtNodeCount(); - int groupOffsetPos = nodeArrayHeadPos + BinaryDictIOUtils.getPtNodeCountSize(count); - for (int i = count; i > 0; --i) { // Scan the array of PtNode. - PtNodeInfo info = dictDecoder.readPtNode(groupOffsetPos, options); - if (BinaryDictIOUtils.isMovedPtNode(info.mFlags, options)) continue; - ArrayList<WeightedString> shortcutTargets = info.mShortcutTargets; - ArrayList<WeightedString> bigrams = null; - if (null != info.mBigrams) { - bigrams = new ArrayList<WeightedString>(); - for (PendingAttribute bigram : info.mBigrams) { - final WeightedString word = getWordAtPosition(dictDecoder, headerSize, - bigram.mAddress, options); - final int reconstructedFrequency = - BinaryDictIOUtils.reconstructBigramFrequency(word.mFrequency, - bigram.mFrequency); - bigrams.add(new WeightedString(word.mWord, reconstructedFrequency)); - } - } - if (BinaryDictIOUtils.hasChildrenAddress(info.mChildrenAddress)) { - PtNodeArray children = reverseNodeArrayMap.get(info.mChildrenAddress); - if (null == children) { - final int currentPosition = dictDecoder.getPosition(); - dictDecoder.setPosition(info.mChildrenAddress); - children = readNodeArray(dictDecoder, headerSize, reverseNodeArrayMap, - reversePtNodeMap, options); - dictDecoder.setPosition(currentPosition); - } - nodeArrayContents.add( - new PtNode(info.mCharacters, shortcutTargets, bigrams, - info.mFrequency, - 0 != (info.mFlags & FormatSpec.FLAG_IS_NOT_A_WORD), - 0 != (info.mFlags & FormatSpec.FLAG_IS_BLACKLISTED), children)); - } else { - nodeArrayContents.add( - new PtNode(info.mCharacters, shortcutTargets, bigrams, - info.mFrequency, - 0 != (info.mFlags & FormatSpec.FLAG_IS_NOT_A_WORD), - 0 != (info.mFlags & FormatSpec.FLAG_IS_BLACKLISTED))); - } - groupOffsetPos = info.mEndAddress; - } - - // reach the end of the array. - if (options.mSupportsDynamicUpdate) { - final boolean hasValidForwardLink = dictDecoder.readAndFollowForwardLink(); - if (!hasValidForwardLink) break; - } - } while (options.mSupportsDynamicUpdate && dictDecoder.hasNextPtNodeArray()); - - final PtNodeArray nodeArray = new PtNodeArray(nodeArrayContents); - nodeArray.mCachedAddressBeforeUpdate = nodeArrayOriginPos; - nodeArray.mCachedAddressAfterUpdate = nodeArrayOriginPos; - reverseNodeArrayMap.put(nodeArray.mCachedAddressAfterUpdate, nodeArray); - return nodeArray; - } - - /** - * Helper function to get the binary format version from the header. - * @throws IOException - */ - private static int getFormatVersion(final DictBuffer dictBuffer) - throws IOException { - final int magic = dictBuffer.readInt(); - if (FormatSpec.MAGIC_NUMBER == magic) return dictBuffer.readUnsignedShort(); - return FormatSpec.NOT_A_VERSION_NUMBER; - } - - /** - * Helper function to get and validate the binary format version. - * @throws UnsupportedFormatException - * @throws IOException - */ - static int checkFormatVersion(final DictBuffer dictBuffer) - throws IOException, UnsupportedFormatException { - final int version = getFormatVersion(dictBuffer); - if (version < FormatSpec.MINIMUM_SUPPORTED_VERSION - || version > FormatSpec.MAXIMUM_SUPPORTED_VERSION) { - throw new UnsupportedFormatException("This file has version " + version - + ", but this implementation does not support versions above " - + FormatSpec.MAXIMUM_SUPPORTED_VERSION); - } - return version; - } - - /** - * Reads a buffer and returns the memory representation of the dictionary. - * - * This high-level method takes a buffer and reads its contents, populating a - * FusionDictionary structure. The optional dict argument is an existing dictionary to - * which words from the buffer should be added. If it is null, a new dictionary is created. - * - * @param dictDecoder the dict decoder. - * @param dict an optional dictionary to add words to, or null. - * @return the created (or merged) dictionary. - */ - @UsedForTesting - /* package */ static FusionDictionary readDictionaryBinary(final DictDecoder dictDecoder, - final FusionDictionary dict) throws IOException, UnsupportedFormatException { - // Read header - final FileHeader fileHeader = dictDecoder.readHeader(); - - Map<Integer, PtNodeArray> reverseNodeArrayMapping = new TreeMap<Integer, PtNodeArray>(); - Map<Integer, PtNode> reversePtNodeMapping = new TreeMap<Integer, PtNode>(); - final PtNodeArray root = readNodeArray(dictDecoder, fileHeader.mHeaderSize, - reverseNodeArrayMapping, reversePtNodeMapping, fileHeader.mFormatOptions); - - FusionDictionary newDict = new FusionDictionary(root, fileHeader.mDictionaryOptions); - if (null != dict) { - for (final Word w : dict) { - if (w.mIsBlacklistEntry) { - newDict.addBlacklistEntry(w.mWord, w.mShortcutTargets, w.mIsNotAWord); - } else { - newDict.add(w.mWord, w.mFrequency, w.mShortcutTargets, w.mIsNotAWord); - } - } - for (final Word w : dict) { - // By construction a binary dictionary may not have bigrams pointing to - // words that are not also registered as unigrams so we don't have to avoid - // them explicitly here. - for (final WeightedString bigram : w.mBigrams) { - newDict.setBigram(w.mWord, bigram.mWord, bigram.mFrequency); - } - } - } - - return newDict; - } - - /** - * Helper method to pass a file name instead of a File object to isBinaryDictionary. - */ - public static boolean isBinaryDictionary(final String filename) { - final File file = new File(filename); - return isBinaryDictionary(file); - } - - /** - * Basic test to find out whether the file is a binary dictionary or not. - * - * Concretely this only tests the magic number. - * - * @param file The file to test. - * @return true if it's a binary dictionary, false otherwise - */ - public static boolean isBinaryDictionary(final File file) { - FileInputStream inStream = null; - try { - inStream = new FileInputStream(file); - final ByteBuffer buffer = inStream.getChannel().map( - FileChannel.MapMode.READ_ONLY, 0, file.length()); - final int version = getFormatVersion(new ByteBufferDictBuffer(buffer)); - return (version >= FormatSpec.MINIMUM_SUPPORTED_VERSION - && version <= FormatSpec.MAXIMUM_SUPPORTED_VERSION); - } catch (FileNotFoundException e) { - return false; - } catch (IOException e) { - return false; - } finally { - if (inStream != null) { - try { - inStream.close(); - } catch (IOException e) { - // do nothing - } - } - } - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java deleted file mode 100644 index f761829de..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java +++ /dev/null @@ -1,956 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding; -import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode; -import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; - -/** - * Encodes binary files for a FusionDictionary. - * - * All the methods in this class are static. - * - * TODO: Rename this class to DictEncoderUtils. - */ -public class BinaryDictEncoderUtils { - - private static final boolean DBG = MakedictLog.DBG; - - private BinaryDictEncoderUtils() { - // This utility class is not publicly instantiable. - } - - // Arbitrary limit to how much passes we consider address size compression should - // terminate in. At the time of this writing, our largest dictionary completes - // compression in five passes. - // If the number of passes exceeds this number, makedict bails with an exception on - // suspicion that a bug might be causing an infinite loop. - private static final int MAX_PASSES = 24; - - /** - * Compute the binary size of the character array. - * - * If only one character, this is the size of this character. If many, it's the sum of their - * sizes + 1 byte for the terminator. - * - * @param characters the character array - * @return the size of the char array, including the terminator if any - */ - static int getPtNodeCharactersSize(final int[] characters) { - int size = CharEncoding.getCharArraySize(characters); - if (characters.length > 1) size += FormatSpec.PTNODE_TERMINATOR_SIZE; - return size; - } - - /** - * Compute the binary size of the character array in a PtNode - * - * If only one character, this is the size of this character. If many, it's the sum of their - * sizes + 1 byte for the terminator. - * - * @param ptNode the PtNode - * @return the size of the char array, including the terminator if any - */ - private static int getPtNodeCharactersSize(final PtNode ptNode) { - return getPtNodeCharactersSize(ptNode.mChars); - } - - /** - * Compute the binary size of the PtNode count for a node array. - * @param nodeArray the nodeArray - * @return the size of the PtNode count, either 1 or 2 bytes. - */ - private static int getPtNodeCountSize(final PtNodeArray nodeArray) { - return BinaryDictIOUtils.getPtNodeCountSize(nodeArray.mData.size()); - } - - /** - * Compute the size of a shortcut in bytes. - */ - private static int getShortcutSize(final WeightedString shortcut) { - int size = FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE; - final String word = shortcut.mWord; - final int length = word.length(); - for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) { - final int codePoint = word.codePointAt(i); - size += CharEncoding.getCharSize(codePoint); - } - size += FormatSpec.PTNODE_TERMINATOR_SIZE; - return size; - } - - /** - * Compute the size of a shortcut list in bytes. - * - * This is known in advance and does not change according to position in the file - * like address lists do. - */ - static int getShortcutListSize(final ArrayList<WeightedString> shortcutList) { - if (null == shortcutList || shortcutList.isEmpty()) return 0; - int size = FormatSpec.PTNODE_SHORTCUT_LIST_SIZE_SIZE; - for (final WeightedString shortcut : shortcutList) { - size += getShortcutSize(shortcut); - } - return size; - } - - /** - * Compute the maximum size of a PtNode, assuming 3-byte addresses for everything. - * - * @param ptNode the PtNode to compute the size of. - * @param options file format options. - * @return the maximum size of the PtNode. - */ - private static int getPtNodeMaximumSize(final PtNode ptNode, final FormatOptions options) { - int size = getNodeHeaderSize(ptNode, options); - if (ptNode.isTerminal()) { - // If terminal, one byte for the frequency or four bytes for the terminal id. - if (options.mHasTerminalId) { - size += FormatSpec.PTNODE_TERMINAL_ID_SIZE; - } else { - size += FormatSpec.PTNODE_FREQUENCY_SIZE; - } - } - size += FormatSpec.PTNODE_MAX_ADDRESS_SIZE; // For children address - size += getShortcutListSize(ptNode.mShortcutTargets); - if (null != ptNode.mBigrams) { - size += (FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE - + FormatSpec.PTNODE_ATTRIBUTE_MAX_ADDRESS_SIZE) - * ptNode.mBigrams.size(); - } - return size; - } - - /** - * Compute the maximum size of each PtNode of a PtNode array, assuming 3-byte addresses for - * everything, and caches it in the `mCachedSize' member of the nodes; deduce the size of - * the containing node array, and cache it it its 'mCachedSize' member. - * - * @param ptNodeArray the node array to compute the maximum size of. - * @param options file format options. - */ - private static void calculatePtNodeArrayMaximumSize(final PtNodeArray ptNodeArray, - final FormatOptions options) { - int size = getPtNodeCountSize(ptNodeArray); - for (PtNode node : ptNodeArray.mData) { - final int nodeSize = getPtNodeMaximumSize(node, options); - node.mCachedSize = nodeSize; - size += nodeSize; - } - if (options.mSupportsDynamicUpdate) { - size += FormatSpec.FORWARD_LINK_ADDRESS_SIZE; - } - ptNodeArray.mCachedSize = size; - } - - /** - * Compute the size of the header (flag + [parent address] + characters size) of a PtNode. - * - * @param ptNode the PtNode of which to compute the size of the header - * @param options file format options. - */ - private static int getNodeHeaderSize(final PtNode ptNode, final FormatOptions options) { - if (BinaryDictIOUtils.supportsDynamicUpdate(options)) { - return FormatSpec.PTNODE_FLAGS_SIZE + FormatSpec.PARENT_ADDRESS_SIZE - + getPtNodeCharactersSize(ptNode); - } else { - return FormatSpec.PTNODE_FLAGS_SIZE + getPtNodeCharactersSize(ptNode); - } - } - - /** - * Compute the size, in bytes, that an address will occupy. - * - * This can be used either for children addresses (which are always positive) or for - * attribute, which may be positive or negative but - * store their sign bit separately. - * - * @param address the address - * @return the byte size. - */ - static int getByteSize(final int address) { - assert(address <= FormatSpec.UINT24_MAX); - if (!BinaryDictIOUtils.hasChildrenAddress(address)) { - return 0; - } else if (Math.abs(address) <= FormatSpec.UINT8_MAX) { - return 1; - } else if (Math.abs(address) <= FormatSpec.UINT16_MAX) { - return 2; - } else { - return 3; - } - } - - static int writeUIntToBuffer(final byte[] buffer, int position, final int value, - final int size) { - switch(size) { - case 4: - buffer[position++] = (byte) ((value >> 24) & 0xFF); - /* fall through */ - case 3: - buffer[position++] = (byte) ((value >> 16) & 0xFF); - /* fall through */ - case 2: - buffer[position++] = (byte) ((value >> 8) & 0xFF); - /* fall through */ - case 1: - buffer[position++] = (byte) (value & 0xFF); - break; - default: - /* nop */ - } - return position; - } - - static void writeUIntToStream(final OutputStream stream, final int value, final int size) - throws IOException { - switch(size) { - case 4: - stream.write((value >> 24) & 0xFF); - /* fall through */ - case 3: - stream.write((value >> 16) & 0xFF); - /* fall through */ - case 2: - stream.write((value >> 8) & 0xFF); - /* fall through */ - case 1: - stream.write(value & 0xFF); - break; - default: - /* nop */ - } - } - - // End utility methods - - // This method is responsible for finding a nice ordering of the nodes that favors run-time - // cache performance and dictionary size. - /* package for tests */ static ArrayList<PtNodeArray> flattenTree( - final PtNodeArray rootNodeArray) { - final int treeSize = FusionDictionary.countPtNodes(rootNodeArray); - MakedictLog.i("Counted nodes : " + treeSize); - final ArrayList<PtNodeArray> flatTree = new ArrayList<PtNodeArray>(treeSize); - return flattenTreeInner(flatTree, rootNodeArray); - } - - private static ArrayList<PtNodeArray> flattenTreeInner(final ArrayList<PtNodeArray> list, - final PtNodeArray ptNodeArray) { - // Removing the node is necessary if the tails are merged, because we would then - // add the same node several times when we only want it once. A number of places in - // the code also depends on any node being only once in the list. - // Merging tails can only be done if there are no attributes. Searching for attributes - // in LatinIME code depends on a total breadth-first ordering, which merging tails - // breaks. If there are no attributes, it should be fine (and reduce the file size) - // to merge tails, and removing the node from the list would be necessary. However, - // we don't merge tails because breaking the breadth-first ordering would result in - // extreme overhead at bigram lookup time (it would make the search function O(n) instead - // of the current O(log(n)), where n=number of nodes in the dictionary which is pretty - // high). - // If no nodes are ever merged, we can't have the same node twice in the list, hence - // searching for duplicates in unnecessary. It is also very performance consuming, - // since `list' is an ArrayList so it's an O(n) operation that runs on all nodes, making - // this simple list.remove operation O(n*n) overall. On Android this overhead is very - // high. - // For future reference, the code to remove duplicate is a simple : list.remove(node); - list.add(ptNodeArray); - final ArrayList<PtNode> branches = ptNodeArray.mData; - for (PtNode ptNode : branches) { - if (null != ptNode.mChildren) flattenTreeInner(list, ptNode.mChildren); - } - return list; - } - - /** - * Get the offset from a position inside a current node array to a target node array, during - * update. - * - * If the current node array is before the target node array, the target node array has not - * been updated yet, so we should return the offset from the old position of the current node - * array to the old position of the target node array. If on the other hand the target is - * before the current node array, it already has been updated, so we should return the offset - * from the new position in the current node array to the new position in the target node - * array. - * - * @param currentNodeArray node array containing the PtNode where the offset will be written - * @param offsetFromStartOfCurrentNodeArray offset, in bytes, from the start of currentNodeArray - * @param targetNodeArray the target node array to get the offset to - * @return the offset to the target node array - */ - private static int getOffsetToTargetNodeArrayDuringUpdate(final PtNodeArray currentNodeArray, - final int offsetFromStartOfCurrentNodeArray, final PtNodeArray targetNodeArray) { - final boolean isTargetBeforeCurrent = (targetNodeArray.mCachedAddressBeforeUpdate - < currentNodeArray.mCachedAddressBeforeUpdate); - if (isTargetBeforeCurrent) { - return targetNodeArray.mCachedAddressAfterUpdate - - (currentNodeArray.mCachedAddressAfterUpdate - + offsetFromStartOfCurrentNodeArray); - } else { - return targetNodeArray.mCachedAddressBeforeUpdate - - (currentNodeArray.mCachedAddressBeforeUpdate - + offsetFromStartOfCurrentNodeArray); - } - } - - /** - * Get the offset from a position inside a current node array to a target PtNode, during - * update. - * - * @param currentNodeArray node array containing the PtNode where the offset will be written - * @param offsetFromStartOfCurrentNodeArray offset, in bytes, from the start of currentNodeArray - * @param targetPtNode the target PtNode to get the offset to - * @return the offset to the target PtNode - */ - // TODO: is there any way to factorize this method with the one above? - private static int getOffsetToTargetPtNodeDuringUpdate(final PtNodeArray currentNodeArray, - final int offsetFromStartOfCurrentNodeArray, final PtNode targetPtNode) { - final int oldOffsetBasePoint = currentNodeArray.mCachedAddressBeforeUpdate - + offsetFromStartOfCurrentNodeArray; - final boolean isTargetBeforeCurrent = (targetPtNode.mCachedAddressBeforeUpdate - < oldOffsetBasePoint); - // If the target is before the current node array, then its address has already been - // updated. We can use the AfterUpdate member, and compare it to our own member after - // update. Otherwise, the AfterUpdate member is not updated yet, so we need to use the - // BeforeUpdate member, and of course we have to compare this to our own address before - // update. - if (isTargetBeforeCurrent) { - final int newOffsetBasePoint = currentNodeArray.mCachedAddressAfterUpdate - + offsetFromStartOfCurrentNodeArray; - return targetPtNode.mCachedAddressAfterUpdate - newOffsetBasePoint; - } else { - return targetPtNode.mCachedAddressBeforeUpdate - oldOffsetBasePoint; - } - } - - /** - * Computes the actual node array size, based on the cached addresses of the children nodes. - * - * Each node array stores its tentative address. During dictionary address computing, these - * are not final, but they can be used to compute the node array size (the node array size - * depends on the address of the children because the number of bytes necessary to store an - * address depends on its numeric value. The return value indicates whether the node array - * contents (as in, any of the addresses stored in the cache fields) have changed with - * respect to their previous value. - * - * @param ptNodeArray the node array to compute the size of. - * @param dict the dictionary in which the word/attributes are to be found. - * @param formatOptions file format options. - * @return false if none of the cached addresses inside the node array changed, true otherwise. - */ - private static boolean computeActualPtNodeArraySize(final PtNodeArray ptNodeArray, - final FusionDictionary dict, final FormatOptions formatOptions) { - boolean changed = false; - int size = getPtNodeCountSize(ptNodeArray); - for (PtNode ptNode : ptNodeArray.mData) { - ptNode.mCachedAddressAfterUpdate = ptNodeArray.mCachedAddressAfterUpdate + size; - if (ptNode.mCachedAddressAfterUpdate != ptNode.mCachedAddressBeforeUpdate) { - changed = true; - } - int nodeSize = getNodeHeaderSize(ptNode, formatOptions); - if (ptNode.isTerminal()) { - if (formatOptions.mHasTerminalId) { - nodeSize += FormatSpec.PTNODE_TERMINAL_ID_SIZE; - } else { - nodeSize += FormatSpec.PTNODE_FREQUENCY_SIZE; - } - } - if (formatOptions.mSupportsDynamicUpdate) { - nodeSize += FormatSpec.SIGNED_CHILDREN_ADDRESS_SIZE; - } else if (null != ptNode.mChildren) { - nodeSize += getByteSize(getOffsetToTargetNodeArrayDuringUpdate(ptNodeArray, - nodeSize + size, ptNode.mChildren)); - } - if (formatOptions.mVersion < FormatSpec.FIRST_VERSION_WITH_TERMINAL_ID) { - nodeSize += getShortcutListSize(ptNode.mShortcutTargets); - if (null != ptNode.mBigrams) { - for (WeightedString bigram : ptNode.mBigrams) { - final int offset = getOffsetToTargetPtNodeDuringUpdate(ptNodeArray, - nodeSize + size + FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE, - FusionDictionary.findWordInTree(dict.mRootNodeArray, bigram.mWord)); - nodeSize += getByteSize(offset) + FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE; - } - } - } - ptNode.mCachedSize = nodeSize; - size += nodeSize; - } - if (formatOptions.mSupportsDynamicUpdate) { - size += FormatSpec.FORWARD_LINK_ADDRESS_SIZE; - } - if (ptNodeArray.mCachedSize != size) { - ptNodeArray.mCachedSize = size; - changed = true; - } - return changed; - } - - /** - * Initializes the cached addresses of node arrays and their containing nodes from their size. - * - * @param flatNodes the list of node arrays. - * @param formatOptions file format options. - * @return the byte size of the entire stack. - */ - private static int initializePtNodeArraysCachedAddresses(final ArrayList<PtNodeArray> flatNodes, - final FormatOptions formatOptions) { - int nodeArrayOffset = 0; - for (final PtNodeArray nodeArray : flatNodes) { - nodeArray.mCachedAddressBeforeUpdate = nodeArrayOffset; - int nodeCountSize = getPtNodeCountSize(nodeArray); - int nodeffset = 0; - for (final PtNode ptNode : nodeArray.mData) { - ptNode.mCachedAddressBeforeUpdate = ptNode.mCachedAddressAfterUpdate = - nodeCountSize + nodeArrayOffset + nodeffset; - nodeffset += ptNode.mCachedSize; - } - nodeArrayOffset += nodeArray.mCachedSize; - } - return nodeArrayOffset; - } - - /** - * Updates the cached addresses of node arrays after recomputing their new positions. - * - * @param flatNodes the list of node arrays. - */ - private static void updatePtNodeArraysCachedAddresses(final ArrayList<PtNodeArray> flatNodes) { - for (final PtNodeArray nodeArray : flatNodes) { - nodeArray.mCachedAddressBeforeUpdate = nodeArray.mCachedAddressAfterUpdate; - for (final PtNode ptNode : nodeArray.mData) { - ptNode.mCachedAddressBeforeUpdate = ptNode.mCachedAddressAfterUpdate; - } - } - } - - /** - * Compute the cached parent addresses after all has been updated. - * - * The parent addresses are used by some binary formats at write-to-disk time. Not all formats - * need them. In particular, version 2 does not need them, and version 3 does. - * - * @param flatNodes the flat array of node arrays to fill in - */ - private static void computeParentAddresses(final ArrayList<PtNodeArray> flatNodes) { - for (final PtNodeArray nodeArray : flatNodes) { - for (final PtNode ptNode : nodeArray.mData) { - if (null != ptNode.mChildren) { - // Assign my address to children's parent address - // Here BeforeUpdate and AfterUpdate addresses have the same value, so it - // does not matter which we use. - ptNode.mChildren.mCachedParentAddress = ptNode.mCachedAddressAfterUpdate - - ptNode.mChildren.mCachedAddressAfterUpdate; - } - } - } - } - - /** - * Compute the addresses and sizes of an ordered list of PtNode arrays. - * - * This method takes a list of PtNode arrays and will update their cached address and size - * values so that they can be written into a file. It determines the smallest size each of the - * PtNode arrays can be given the addresses of its children and attributes, and store that into - * each PtNode. - * The order of the PtNode is given by the order of the array. This method makes no effort - * to find a good order; it only mechanically computes the size this order results in. - * - * @param dict the dictionary - * @param flatNodes the ordered list of PtNode arrays - * @param formatOptions file format options. - * @return the same array it was passed. The nodes have been updated for address and size. - */ - /* package */ static ArrayList<PtNodeArray> computeAddresses(final FusionDictionary dict, - final ArrayList<PtNodeArray> flatNodes, final FormatOptions formatOptions) { - // First get the worst possible sizes and offsets - for (final PtNodeArray n : flatNodes) calculatePtNodeArrayMaximumSize(n, formatOptions); - final int offset = initializePtNodeArraysCachedAddresses(flatNodes, formatOptions); - - MakedictLog.i("Compressing the array addresses. Original size : " + offset); - MakedictLog.i("(Recursively seen size : " + offset + ")"); - - int passes = 0; - boolean changesDone = false; - do { - changesDone = false; - int ptNodeArrayStartOffset = 0; - for (final PtNodeArray ptNodeArray : flatNodes) { - ptNodeArray.mCachedAddressAfterUpdate = ptNodeArrayStartOffset; - final int oldNodeArraySize = ptNodeArray.mCachedSize; - final boolean changed = - computeActualPtNodeArraySize(ptNodeArray, dict, formatOptions); - final int newNodeArraySize = ptNodeArray.mCachedSize; - if (oldNodeArraySize < newNodeArraySize) { - throw new RuntimeException("Increased size ?!"); - } - ptNodeArrayStartOffset += newNodeArraySize; - changesDone |= changed; - } - updatePtNodeArraysCachedAddresses(flatNodes); - ++passes; - if (passes > MAX_PASSES) throw new RuntimeException("Too many passes - probably a bug"); - } while (changesDone); - - if (formatOptions.mSupportsDynamicUpdate) { - computeParentAddresses(flatNodes); - } - final PtNodeArray lastPtNodeArray = flatNodes.get(flatNodes.size() - 1); - MakedictLog.i("Compression complete in " + passes + " passes."); - MakedictLog.i("After address compression : " - + (lastPtNodeArray.mCachedAddressAfterUpdate + lastPtNodeArray.mCachedSize)); - - return flatNodes; - } - - /** - * Sanity-checking method. - * - * This method checks a list of PtNode arrays for juxtaposition, that is, it will do - * nothing if each node array's cached address is actually the previous node array's address - * plus the previous node's size. - * If this is not the case, it will throw an exception. - * - * @param arrays the list of node arrays to check - */ - /* package */ static void checkFlatPtNodeArrayList(final ArrayList<PtNodeArray> arrays) { - int offset = 0; - int index = 0; - for (final PtNodeArray ptNodeArray : arrays) { - // BeforeUpdate and AfterUpdate addresses are the same here, so it does not matter - // which we use. - if (ptNodeArray.mCachedAddressAfterUpdate != offset) { - throw new RuntimeException("Wrong address for node " + index - + " : expected " + offset + ", got " + - ptNodeArray.mCachedAddressAfterUpdate); - } - ++index; - offset += ptNodeArray.mCachedSize; - } - } - - /** - * Helper method to write a children position to a file. - * - * @param buffer the buffer to write to. - * @param index the index in the buffer to write the address to. - * @param position the position to write. - * @return the size in bytes the address actually took. - */ - /* package */ static int writeChildrenPosition(final byte[] buffer, int index, - final int position) { - switch (getByteSize(position)) { - case 1: - buffer[index++] = (byte)position; - return 1; - case 2: - buffer[index++] = (byte)(0xFF & (position >> 8)); - buffer[index++] = (byte)(0xFF & position); - return 2; - case 3: - buffer[index++] = (byte)(0xFF & (position >> 16)); - buffer[index++] = (byte)(0xFF & (position >> 8)); - buffer[index++] = (byte)(0xFF & position); - return 3; - case 0: - return 0; - default: - throw new RuntimeException("Position " + position + " has a strange size"); - } - } - - /** - * Helper method to write a signed children position to a file. - * - * @param buffer the buffer to write to. - * @param index the index in the buffer to write the address to. - * @param position the position to write. - * @return the size in bytes the address actually took. - */ - /* package */ static int writeSignedChildrenPosition(final byte[] buffer, int index, - final int position) { - if (!BinaryDictIOUtils.hasChildrenAddress(position)) { - buffer[index] = buffer[index + 1] = buffer[index + 2] = 0; - } else { - final int absPosition = Math.abs(position); - buffer[index++] = - (byte)((position < 0 ? FormatSpec.MSB8 : 0) | (0xFF & (absPosition >> 16))); - buffer[index++] = (byte)(0xFF & (absPosition >> 8)); - buffer[index++] = (byte)(0xFF & absPosition); - } - return 3; - } - - /** - * Makes the flag value for a PtNode. - * - * @param hasMultipleChars whether the PtNode has multiple chars. - * @param isTerminal whether the PtNode is terminal. - * @param childrenAddressSize the size of a children address. - * @param hasShortcuts whether the PtNode has shortcuts. - * @param hasBigrams whether the PtNode has bigrams. - * @param isNotAWord whether the PtNode is not a word. - * @param isBlackListEntry whether the PtNode is a blacklist entry. - * @param formatOptions file format options. - * @return the flags - */ - static int makePtNodeFlags(final boolean hasMultipleChars, final boolean isTerminal, - final int childrenAddressSize, final boolean hasShortcuts, final boolean hasBigrams, - final boolean isNotAWord, final boolean isBlackListEntry, - final FormatOptions formatOptions) { - byte flags = 0; - if (hasMultipleChars) flags |= FormatSpec.FLAG_HAS_MULTIPLE_CHARS; - if (isTerminal) flags |= FormatSpec.FLAG_IS_TERMINAL; - if (formatOptions.mSupportsDynamicUpdate) { - flags |= FormatSpec.FLAG_IS_NOT_MOVED; - } else if (true) { - switch (childrenAddressSize) { - case 1: - flags |= FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE; - break; - case 2: - flags |= FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES; - break; - case 3: - flags |= FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES; - break; - case 0: - flags |= FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS; - break; - default: - throw new RuntimeException("Node with a strange address"); - } - } - if (hasShortcuts) flags |= FormatSpec.FLAG_HAS_SHORTCUT_TARGETS; - if (hasBigrams) flags |= FormatSpec.FLAG_HAS_BIGRAMS; - if (isNotAWord) flags |= FormatSpec.FLAG_IS_NOT_A_WORD; - if (isBlackListEntry) flags |= FormatSpec.FLAG_IS_BLACKLISTED; - return flags; - } - - /* package */ static byte makePtNodeFlags(final PtNode node, final int childrenOffset, - final FormatOptions formatOptions) { - return (byte) makePtNodeFlags(node.mChars.length > 1, node.mFrequency >= 0, - getByteSize(childrenOffset), - node.mShortcutTargets != null && !node.mShortcutTargets.isEmpty(), - node.mBigrams != null, node.mIsNotAWord, node.mIsBlacklistEntry, formatOptions); - } - - /** - * Makes the flag value for a bigram. - * - * @param more whether there are more bigrams after this one. - * @param offset the offset of the bigram. - * @param bigramFrequency the frequency of the bigram, 0..255. - * @param unigramFrequency the unigram frequency of the same word, 0..255. - * @param word the second bigram, for debugging purposes - * @return the flags - */ - /* package */ static final int makeBigramFlags(final boolean more, final int offset, - int bigramFrequency, final int unigramFrequency, final String word) { - int bigramFlags = (more ? FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT : 0) - + (offset < 0 ? FormatSpec.FLAG_BIGRAM_ATTR_OFFSET_NEGATIVE : 0); - switch (getByteSize(offset)) { - case 1: - bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE; - break; - case 2: - bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES; - break; - case 3: - bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES; - break; - default: - throw new RuntimeException("Strange offset size"); - } - if (unigramFrequency > bigramFrequency) { - MakedictLog.e("Unigram freq is superior to bigram freq for \"" + word - + "\". Bigram freq is " + bigramFrequency + ", unigram freq for " - + word + " is " + unigramFrequency); - bigramFrequency = unigramFrequency; - } - // We compute the difference between 255 (which means probability = 1) and the - // unigram score. We split this into a number of discrete steps. - // Now, the steps are numbered 0~15; 0 represents an increase of 1 step while 15 - // represents an increase of 16 steps: a value of 15 will be interpreted as the median - // value of the 16th step. In all justice, if the bigram frequency is low enough to be - // rounded below the first step (which means it is less than half a step higher than the - // unigram frequency) then the unigram frequency itself is the best approximation of the - // bigram freq that we could possibly supply, hence we should *not* include this bigram - // in the file at all. - // until this is done, we'll write 0 and slightly overestimate this case. - // In other words, 0 means "between 0.5 step and 1.5 step", 1 means "between 1.5 step - // and 2.5 steps", and 15 means "between 15.5 steps and 16.5 steps". So we want to - // divide our range [unigramFreq..MAX_TERMINAL_FREQUENCY] in 16.5 steps to get the - // step size. Then we compute the start of the first step (the one where value 0 starts) - // by adding half-a-step to the unigramFrequency. From there, we compute the integer - // number of steps to the bigramFrequency. One last thing: we want our steps to include - // their lower bound and exclude their higher bound so we need to have the first step - // start at exactly 1 unit higher than floor(unigramFreq + half a step). - // Note : to reconstruct the score, the dictionary reader will need to divide - // MAX_TERMINAL_FREQUENCY - unigramFreq by 16.5 likewise to get the value of the step, - // and add (discretizedFrequency + 0.5 + 0.5) times this value to get the best - // approximation. (0.5 to get the first step start, and 0.5 to get the middle of the - // step pointed by the discretized frequency. - final float stepSize = - (FormatSpec.MAX_TERMINAL_FREQUENCY - unigramFrequency) - / (1.5f + FormatSpec.MAX_BIGRAM_FREQUENCY); - final float firstStepStart = 1 + unigramFrequency + (stepSize / 2.0f); - final int discretizedFrequency = (int)((bigramFrequency - firstStepStart) / stepSize); - // If the bigram freq is less than half-a-step higher than the unigram freq, we get -1 - // here. The best approximation would be the unigram freq itself, so we should not - // include this bigram in the dictionary. For now, register as 0, and live with the - // small over-estimation that we get in this case. TODO: actually remove this bigram - // if discretizedFrequency < 0. - final int finalBigramFrequency = discretizedFrequency > 0 ? discretizedFrequency : 0; - bigramFlags += finalBigramFrequency & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY; - return bigramFlags; - } - - /** - * Makes the 2-byte value for options flags. - */ - private static final int makeOptionsValue(final FusionDictionary dictionary, - final FormatOptions formatOptions) { - final DictionaryOptions options = dictionary.mOptions; - final boolean hasBigrams = dictionary.hasBigrams(); - return (options.mFrenchLigatureProcessing ? FormatSpec.FRENCH_LIGATURE_PROCESSING_FLAG : 0) - + (options.mGermanUmlautProcessing ? FormatSpec.GERMAN_UMLAUT_PROCESSING_FLAG : 0) - + (hasBigrams ? FormatSpec.CONTAINS_BIGRAMS_FLAG : 0) - + (formatOptions.mSupportsDynamicUpdate ? FormatSpec.SUPPORTS_DYNAMIC_UPDATE : 0); - } - - /** - * Makes the flag value for a shortcut. - * - * @param more whether there are more attributes after this one. - * @param frequency the frequency of the attribute, 0..15 - * @return the flags - */ - static final int makeShortcutFlags(final boolean more, final int frequency) { - return (more ? FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT : 0) - + (frequency & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY); - } - - /* package */ static final int writeParentAddress(final byte[] buffer, final int index, - final int address, final FormatOptions formatOptions) { - if (BinaryDictIOUtils.supportsDynamicUpdate(formatOptions)) { - if (address == FormatSpec.NO_PARENT_ADDRESS) { - buffer[index] = buffer[index + 1] = buffer[index + 2] = 0; - } else { - final int absAddress = Math.abs(address); - assert(absAddress <= FormatSpec.SINT24_MAX); - buffer[index] = (byte)((address < 0 ? FormatSpec.MSB8 : 0) - | ((absAddress >> 16) & 0xFF)); - buffer[index + 1] = (byte)((absAddress >> 8) & 0xFF); - buffer[index + 2] = (byte)(absAddress & 0xFF); - } - return index + 3; - } else { - return index; - } - } - - /* package */ static final int getChildrenPosition(final PtNode ptNode, - final FormatOptions formatOptions) { - int positionOfChildrenPosField = ptNode.mCachedAddressAfterUpdate - + getNodeHeaderSize(ptNode, formatOptions); - if (ptNode.isTerminal()) { - // A terminal node has either the terminal id or the frequency. - // If positionOfChildrenPosField is incorrect, we may crash when jumping to the children - // position. - if (formatOptions.mHasTerminalId) { - positionOfChildrenPosField += FormatSpec.PTNODE_TERMINAL_ID_SIZE; - } else { - positionOfChildrenPosField += FormatSpec.PTNODE_FREQUENCY_SIZE; - } - } - return null == ptNode.mChildren ? FormatSpec.NO_CHILDREN_ADDRESS - : ptNode.mChildren.mCachedAddressAfterUpdate - positionOfChildrenPosField; - } - - /** - * Write a PtNodeArray. The PtNodeArray is expected to have its final position cached. - * - * @param dict the dictionary the node array is a part of (for relative offsets). - * @param dictEncoder the dictionary encoder. - * @param ptNodeArray the node array to write. - * @param formatOptions file format options. - */ - @SuppressWarnings("unused") - /* package */ static void writePlacedPtNodeArray(final FusionDictionary dict, - final DictEncoder dictEncoder, final PtNodeArray ptNodeArray, - final FormatOptions formatOptions) { - // TODO: Make the code in common with BinaryDictIOUtils#writePtNode - dictEncoder.setPosition(ptNodeArray.mCachedAddressAfterUpdate); - - final int ptNodeCount = ptNodeArray.mData.size(); - dictEncoder.writePtNodeCount(ptNodeCount); - final int parentPosition = - (ptNodeArray.mCachedParentAddress == FormatSpec.NO_PARENT_ADDRESS) - ? FormatSpec.NO_PARENT_ADDRESS - : ptNodeArray.mCachedParentAddress + ptNodeArray.mCachedAddressAfterUpdate; - for (int i = 0; i < ptNodeCount; ++i) { - final PtNode ptNode = ptNodeArray.mData.get(i); - if (dictEncoder.getPosition() != ptNode.mCachedAddressAfterUpdate) { - throw new RuntimeException("Bug: write index is not the same as the cached address " - + "of the node : " + dictEncoder.getPosition() + " <> " - + ptNode.mCachedAddressAfterUpdate); - } - // Sanity checks. - if (DBG && ptNode.mFrequency > FormatSpec.MAX_TERMINAL_FREQUENCY) { - throw new RuntimeException("A node has a frequency > " - + FormatSpec.MAX_TERMINAL_FREQUENCY - + " : " + ptNode.mFrequency); - } - dictEncoder.writePtNode(ptNode, parentPosition, formatOptions, dict); - } - if (formatOptions.mSupportsDynamicUpdate) { - dictEncoder.writeForwardLinkAddress(FormatSpec.NO_FORWARD_LINK_ADDRESS); - } - if (dictEncoder.getPosition() != ptNodeArray.mCachedAddressAfterUpdate - + ptNodeArray.mCachedSize) { - throw new RuntimeException("Not the same size : written " - + (dictEncoder.getPosition() - ptNodeArray.mCachedAddressAfterUpdate) - + " bytes from a node that should have " + ptNodeArray.mCachedSize + " bytes"); - } - } - - /** - * Dumps a collection of useful statistics about a list of PtNode arrays. - * - * This prints purely informative stuff, like the total estimated file size, the - * number of PtNode arrays, of PtNodes, the repartition of each address size, etc - * - * @param ptNodeArrays the list of PtNode arrays. - */ - /* package */ static void showStatistics(ArrayList<PtNodeArray> ptNodeArrays) { - int firstTerminalAddress = Integer.MAX_VALUE; - int lastTerminalAddress = Integer.MIN_VALUE; - int size = 0; - int ptNodes = 0; - int maxNodes = 0; - int maxRuns = 0; - for (final PtNodeArray ptNodeArray : ptNodeArrays) { - if (maxNodes < ptNodeArray.mData.size()) maxNodes = ptNodeArray.mData.size(); - for (final PtNode ptNode : ptNodeArray.mData) { - ++ptNodes; - if (ptNode.mChars.length > maxRuns) maxRuns = ptNode.mChars.length; - if (ptNode.mFrequency >= 0) { - if (ptNodeArray.mCachedAddressAfterUpdate < firstTerminalAddress) - firstTerminalAddress = ptNodeArray.mCachedAddressAfterUpdate; - if (ptNodeArray.mCachedAddressAfterUpdate > lastTerminalAddress) - lastTerminalAddress = ptNodeArray.mCachedAddressAfterUpdate; - } - } - if (ptNodeArray.mCachedAddressAfterUpdate + ptNodeArray.mCachedSize > size) { - size = ptNodeArray.mCachedAddressAfterUpdate + ptNodeArray.mCachedSize; - } - } - final int[] ptNodeCounts = new int[maxNodes + 1]; - final int[] runCounts = new int[maxRuns + 1]; - for (final PtNodeArray ptNodeArray : ptNodeArrays) { - ++ptNodeCounts[ptNodeArray.mData.size()]; - for (final PtNode ptNode : ptNodeArray.mData) { - ++runCounts[ptNode.mChars.length]; - } - } - - MakedictLog.i("Statistics:\n" - + " total file size " + size + "\n" - + " " + ptNodeArrays.size() + " node arrays\n" - + " " + ptNodes + " PtNodes (" + ((float)ptNodes / ptNodeArrays.size()) - + " PtNodes per node)\n" - + " first terminal at " + firstTerminalAddress + "\n" - + " last terminal at " + lastTerminalAddress + "\n" - + " PtNode stats : max = " + maxNodes); - for (int i = 0; i < ptNodeCounts.length; ++i) { - MakedictLog.i(" " + i + " : " + ptNodeCounts[i]); - } - MakedictLog.i(" Character run stats : max = " + maxRuns); - for (int i = 0; i < runCounts.length; ++i) { - MakedictLog.i(" " + i + " : " + runCounts[i]); - } - } - - /** - * Writes a file header to an output stream. - * - * @param destination the stream to write the file header to. - * @param dict the dictionary to write. - * @param formatOptions file format options. - * @return the size of the header. - */ - /* package */ static int writeDictionaryHeader(final OutputStream destination, - final FusionDictionary dict, final FormatOptions formatOptions) - throws IOException, UnsupportedFormatException { - final int version = formatOptions.mVersion; - if (version < FormatSpec.MINIMUM_SUPPORTED_VERSION - || version > FormatSpec.MAXIMUM_SUPPORTED_VERSION) { - throw new UnsupportedFormatException("Requested file format version " + version - + ", but this implementation only supports versions " - + FormatSpec.MINIMUM_SUPPORTED_VERSION + " through " - + FormatSpec.MAXIMUM_SUPPORTED_VERSION); - } - - ByteArrayOutputStream headerBuffer = new ByteArrayOutputStream(256); - - // The magic number in big-endian order. - // Magic number for all versions. - headerBuffer.write((byte) (0xFF & (FormatSpec.MAGIC_NUMBER >> 24))); - headerBuffer.write((byte) (0xFF & (FormatSpec.MAGIC_NUMBER >> 16))); - headerBuffer.write((byte) (0xFF & (FormatSpec.MAGIC_NUMBER >> 8))); - headerBuffer.write((byte) (0xFF & FormatSpec.MAGIC_NUMBER)); - // Dictionary version. - headerBuffer.write((byte) (0xFF & (version >> 8))); - headerBuffer.write((byte) (0xFF & version)); - - // Options flags - final int options = makeOptionsValue(dict, formatOptions); - headerBuffer.write((byte) (0xFF & (options >> 8))); - headerBuffer.write((byte) (0xFF & options)); - final int headerSizeOffset = headerBuffer.size(); - // Placeholder to be written later with header size. - for (int i = 0; i < 4; ++i) { - headerBuffer.write(0); - } - // Write out the options. - for (final String key : dict.mOptions.mAttributes.keySet()) { - final String value = dict.mOptions.mAttributes.get(key); - CharEncoding.writeString(headerBuffer, key); - CharEncoding.writeString(headerBuffer, value); - } - final int size = headerBuffer.size(); - final byte[] bytes = headerBuffer.toByteArray(); - // Write out the header size. - bytes[headerSizeOffset] = (byte) (0xFF & (size >> 24)); - bytes[headerSizeOffset + 1] = (byte) (0xFF & (size >> 16)); - bytes[headerSizeOffset + 2] = (byte) (0xFF & (size >> 8)); - bytes[headerSizeOffset + 3] = (byte) (0xFF & (size >> 0)); - destination.write(bytes); - - headerBuffer.close(); - return size; - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java deleted file mode 100644 index d5516ef46..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java +++ /dev/null @@ -1,599 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding; -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; -import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; -import com.android.inputmethod.latin.utils.ByteArrayDictBuffer; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.Map; -import java.util.Stack; - -public final class BinaryDictIOUtils { - private static final boolean DBG = false; - - private BinaryDictIOUtils() { - // This utility class is not publicly instantiable. - } - - private static final class Position { - public static final int NOT_READ_PTNODE_COUNT = -1; - - public int mAddress; - public int mNumOfPtNode; - public int mPosition; - public int mLength; - - public Position(int address, int length) { - mAddress = address; - mLength = length; - mNumOfPtNode = NOT_READ_PTNODE_COUNT; - } - } - - /** - * Retrieves all node arrays without recursive call. - */ - private static void readUnigramsAndBigramsBinaryInner(final DictDecoder dictDecoder, - final int headerSize, final Map<Integer, String> words, - final Map<Integer, Integer> frequencies, - final Map<Integer, ArrayList<PendingAttribute>> bigrams, - final FormatOptions formatOptions) { - int[] pushedChars = new int[FormatSpec.MAX_WORD_LENGTH + 1]; - - Stack<Position> stack = new Stack<Position>(); - int index = 0; - - Position initPos = new Position(headerSize, 0); - stack.push(initPos); - - while (!stack.empty()) { - Position p = stack.peek(); - - if (DBG) { - MakedictLog.d("read: address=" + p.mAddress + ", numOfPtNode=" + - p.mNumOfPtNode + ", position=" + p.mPosition + ", length=" + p.mLength); - } - - if (dictDecoder.getPosition() != p.mAddress) dictDecoder.setPosition(p.mAddress); - if (index != p.mLength) index = p.mLength; - - if (p.mNumOfPtNode == Position.NOT_READ_PTNODE_COUNT) { - p.mNumOfPtNode = dictDecoder.readPtNodeCount(); - p.mAddress += getPtNodeCountSize(p.mNumOfPtNode); - p.mPosition = 0; - } - if (p.mNumOfPtNode == 0) { - stack.pop(); - continue; - } - PtNodeInfo info = dictDecoder.readPtNode(p.mAddress, formatOptions); - for (int i = 0; i < info.mCharacters.length; ++i) { - pushedChars[index++] = info.mCharacters[i]; - } - p.mPosition++; - - final boolean isMovedPtNode = isMovedPtNode(info.mFlags, - formatOptions); - final boolean isDeletedPtNode = isDeletedPtNode(info.mFlags, - formatOptions); - if (!isMovedPtNode && !isDeletedPtNode - && info.mFrequency != FusionDictionary.PtNode.NOT_A_TERMINAL) {// found word - words.put(info.mOriginalAddress, new String(pushedChars, 0, index)); - frequencies.put(info.mOriginalAddress, info.mFrequency); - if (info.mBigrams != null) bigrams.put(info.mOriginalAddress, info.mBigrams); - } - - if (p.mPosition == p.mNumOfPtNode) { - if (formatOptions.mSupportsDynamicUpdate) { - final boolean hasValidForwardLinkAddress = - dictDecoder.readAndFollowForwardLink(); - if (hasValidForwardLinkAddress && dictDecoder.hasNextPtNodeArray()) { - // The node array has a forward link. - p.mNumOfPtNode = Position.NOT_READ_PTNODE_COUNT; - p.mAddress = dictDecoder.getPosition(); - } else { - stack.pop(); - } - } else { - stack.pop(); - } - } else { - // The Ptnode array has more PtNodes. - p.mAddress = dictDecoder.getPosition(); - } - - if (!isMovedPtNode && hasChildrenAddress(info.mChildrenAddress)) { - final Position childrenPos = new Position(info.mChildrenAddress, index); - stack.push(childrenPos); - } - } - } - - /** - * Reads unigrams and bigrams from the binary file. - * Doesn't store a full memory representation of the dictionary. - * - * @param dictDecoder the dict decoder. - * @param words the map to store the address as a key and the word as a value. - * @param frequencies the map to store the address as a key and the frequency as a value. - * @param bigrams the map to store the address as a key and the list of address as a value. - * @throws IOException if the file can't be read. - * @throws UnsupportedFormatException if the format of the file is not recognized. - */ - /* package */ static void readUnigramsAndBigramsBinary(final DictDecoder dictDecoder, - final Map<Integer, String> words, final Map<Integer, Integer> frequencies, - final Map<Integer, ArrayList<PendingAttribute>> bigrams) throws IOException, - UnsupportedFormatException { - // Read header - final FileHeader header = dictDecoder.readHeader(); - readUnigramsAndBigramsBinaryInner(dictDecoder, header.mHeaderSize, words, - frequencies, bigrams, header.mFormatOptions); - } - - /** - * Gets the address of the last PtNode of the exact matching word in the dictionary. - * If no match is found, returns NOT_VALID_WORD. - * - * @param dictDecoder the dict decoder. - * @param word the word we search for. - * @return the address of the terminal node. - * @throws IOException if the file can't be read. - * @throws UnsupportedFormatException if the format of the file is not recognized. - */ - @UsedForTesting - /* package */ static int getTerminalPosition(final DictDecoder dictDecoder, - final String word) throws IOException, UnsupportedFormatException { - if (word == null) return FormatSpec.NOT_VALID_WORD; - dictDecoder.setPosition(0); - - final FileHeader header = dictDecoder.readHeader(); - int wordPos = 0; - final int wordLen = word.codePointCount(0, word.length()); - for (int depth = 0; depth < Constants.DICTIONARY_MAX_WORD_LENGTH; ++depth) { - if (wordPos >= wordLen) return FormatSpec.NOT_VALID_WORD; - - do { - final int ptNodeCount = dictDecoder.readPtNodeCount(); - boolean foundNextPtNode = false; - for (int i = 0; i < ptNodeCount; ++i) { - final int ptNodePos = dictDecoder.getPosition(); - final PtNodeInfo currentInfo = dictDecoder.readPtNode(ptNodePos, - header.mFormatOptions); - final boolean isMovedNode = isMovedPtNode(currentInfo.mFlags, - header.mFormatOptions); - final boolean isDeletedNode = isDeletedPtNode(currentInfo.mFlags, - header.mFormatOptions); - if (isMovedNode) continue; - boolean same = true; - for (int p = 0, j = word.offsetByCodePoints(0, wordPos); - p < currentInfo.mCharacters.length; - ++p, j = word.offsetByCodePoints(j, 1)) { - if (wordPos + p >= wordLen - || word.codePointAt(j) != currentInfo.mCharacters[p]) { - same = false; - break; - } - } - - if (same) { - // found the PtNode matches the word. - if (wordPos + currentInfo.mCharacters.length == wordLen) { - if (currentInfo.mFrequency == PtNode.NOT_A_TERMINAL - || isDeletedNode) { - return FormatSpec.NOT_VALID_WORD; - } else { - return ptNodePos; - } - } - wordPos += currentInfo.mCharacters.length; - if (currentInfo.mChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS) { - return FormatSpec.NOT_VALID_WORD; - } - foundNextPtNode = true; - dictDecoder.setPosition(currentInfo.mChildrenAddress); - break; - } - } - - // If we found the next PtNode, it is under the file pointer. - // But if not, we are at the end of this node array so we expect to have - // a forward link address that we need to consult and possibly resume - // search on the next node array in the linked list. - if (foundNextPtNode) break; - if (!header.mFormatOptions.mSupportsDynamicUpdate) { - return FormatSpec.NOT_VALID_WORD; - } - - final boolean hasValidForwardLinkAddress = - dictDecoder.readAndFollowForwardLink(); - if (!hasValidForwardLinkAddress || !dictDecoder.hasNextPtNodeArray()) { - return FormatSpec.NOT_VALID_WORD; - } - } while(true); - } - return FormatSpec.NOT_VALID_WORD; - } - - /** - * @return the size written, in bytes. Always 3 bytes. - */ - static int writeSInt24ToBuffer(final DictBuffer dictBuffer, - final int value) { - final int absValue = Math.abs(value); - dictBuffer.put((byte)(((value < 0 ? 0x80 : 0) | (absValue >> 16)) & 0xFF)); - dictBuffer.put((byte)((absValue >> 8) & 0xFF)); - dictBuffer.put((byte)(absValue & 0xFF)); - return 3; - } - - /** - * @return the size written, in bytes. Always 3 bytes. - */ - static int writeSInt24ToStream(final OutputStream destination, final int value) - throws IOException { - final int absValue = Math.abs(value); - destination.write((byte)(((value < 0 ? 0x80 : 0) | (absValue >> 16)) & 0xFF)); - destination.write((byte)((absValue >> 8) & 0xFF)); - destination.write((byte)(absValue & 0xFF)); - return 3; - } - - /** - * @return the size written, in bytes. 1, 2, or 3 bytes. - */ - private static int writeVariableAddress(final OutputStream destination, final int value) - throws IOException { - switch (BinaryDictEncoderUtils.getByteSize(value)) { - case 1: - destination.write((byte)value); - break; - case 2: - destination.write((byte)(0xFF & (value >> 8))); - destination.write((byte)(0xFF & value)); - break; - case 3: - destination.write((byte)(0xFF & (value >> 16))); - destination.write((byte)(0xFF & (value >> 8))); - destination.write((byte)(0xFF & value)); - break; - } - return BinaryDictEncoderUtils.getByteSize(value); - } - - static void skipString(final DictBuffer dictBuffer, - final boolean hasMultipleChars) { - if (hasMultipleChars) { - int character = CharEncoding.readChar(dictBuffer); - while (character != FormatSpec.INVALID_CHARACTER) { - character = CharEncoding.readChar(dictBuffer); - } - } else { - CharEncoding.readChar(dictBuffer); - } - } - - /** - * Write a string to a stream. - * - * @param destination the stream to write. - * @param word the string to be written. - * @return the size written, in bytes. - * @throws IOException - */ - private static int writeString(final OutputStream destination, final String word) - throws IOException { - int size = 0; - final int length = word.length(); - for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) { - final int codePoint = word.codePointAt(i); - if (CharEncoding.getCharSize(codePoint) == 1) { - destination.write((byte)codePoint); - size++; - } else { - destination.write((byte)(0xFF & (codePoint >> 16))); - destination.write((byte)(0xFF & (codePoint >> 8))); - destination.write((byte)(0xFF & codePoint)); - size += 3; - } - } - destination.write((byte)FormatSpec.PTNODE_CHARACTERS_TERMINATOR); - size += FormatSpec.PTNODE_TERMINATOR_SIZE; - return size; - } - - /** - * Write a PtNode to an output stream from a PtNodeInfo. - * A PtNode is an in-memory representation of a node in the patricia trie. - * A PtNode info is a container for low-level information about how the - * PtNode is stored in the binary format. - * - * @param destination the stream to write. - * @param info the PtNode info to be written. - * @return the size written, in bytes. - */ - private static int writePtNode(final OutputStream destination, final PtNodeInfo info) - throws IOException { - int size = FormatSpec.PTNODE_FLAGS_SIZE; - destination.write((byte)info.mFlags); - final int parentOffset = info.mParentAddress == FormatSpec.NO_PARENT_ADDRESS ? - FormatSpec.NO_PARENT_ADDRESS : info.mParentAddress - info.mOriginalAddress; - size += writeSInt24ToStream(destination, parentOffset); - - for (int i = 0; i < info.mCharacters.length; ++i) { - if (CharEncoding.getCharSize(info.mCharacters[i]) == 1) { - destination.write((byte)info.mCharacters[i]); - size++; - } else { - size += writeSInt24ToStream(destination, info.mCharacters[i]); - } - } - if (info.mCharacters.length > 1) { - destination.write((byte)FormatSpec.PTNODE_CHARACTERS_TERMINATOR); - size++; - } - - if ((info.mFlags & FormatSpec.FLAG_IS_TERMINAL) != 0) { - destination.write((byte)info.mFrequency); - size++; - } - - if (DBG) { - MakedictLog.d("writePtNode origin=" + info.mOriginalAddress + ", size=" + size - + ", child=" + info.mChildrenAddress + ", characters =" - + new String(info.mCharacters, 0, info.mCharacters.length)); - } - final int childrenOffset = info.mChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS ? - 0 : info.mChildrenAddress - (info.mOriginalAddress + size); - writeSInt24ToStream(destination, childrenOffset); - size += FormatSpec.SIGNED_CHILDREN_ADDRESS_SIZE; - - if (info.mShortcutTargets != null && info.mShortcutTargets.size() > 0) { - final int shortcutListSize = - BinaryDictEncoderUtils.getShortcutListSize(info.mShortcutTargets); - destination.write((byte)(shortcutListSize >> 8)); - destination.write((byte)(shortcutListSize & 0xFF)); - size += 2; - final Iterator<WeightedString> shortcutIterator = info.mShortcutTargets.iterator(); - while (shortcutIterator.hasNext()) { - final WeightedString target = shortcutIterator.next(); - destination.write((byte)BinaryDictEncoderUtils.makeShortcutFlags( - shortcutIterator.hasNext(), target.mFrequency)); - size++; - size += writeString(destination, target.mWord); - } - } - - if (info.mBigrams != null) { - // TODO: Consolidate this code with the code that computes the size of the bigram list - // in BinaryDictEncoderUtils#computeActualNodeArraySize - for (int i = 0; i < info.mBigrams.size(); ++i) { - - final int bigramFrequency = info.mBigrams.get(i).mFrequency; - int bigramFlags = (i < info.mBigrams.size() - 1) - ? FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT : 0; - size++; - final int bigramOffset = info.mBigrams.get(i).mAddress - (info.mOriginalAddress - + size); - bigramFlags |= (bigramOffset < 0) ? FormatSpec.FLAG_BIGRAM_ATTR_OFFSET_NEGATIVE : 0; - switch (BinaryDictEncoderUtils.getByteSize(bigramOffset)) { - case 1: - bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE; - break; - case 2: - bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES; - break; - case 3: - bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES; - break; - } - bigramFlags |= bigramFrequency & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY; - destination.write((byte)bigramFlags); - size += writeVariableAddress(destination, Math.abs(bigramOffset)); - } - } - return size; - } - - /** - * Compute the size of the PtNode. - */ - static int computePtNodeSize(final PtNodeInfo info, final FormatOptions formatOptions) { - int size = FormatSpec.PTNODE_FLAGS_SIZE + FormatSpec.PARENT_ADDRESS_SIZE - + BinaryDictEncoderUtils.getPtNodeCharactersSize(info.mCharacters) - + getChildrenAddressSize(info.mFlags, formatOptions); - if ((info.mFlags & FormatSpec.FLAG_IS_TERMINAL) != 0) { - size += FormatSpec.PTNODE_FREQUENCY_SIZE; - } - if (info.mShortcutTargets != null && !info.mShortcutTargets.isEmpty()) { - size += BinaryDictEncoderUtils.getShortcutListSize(info.mShortcutTargets); - } - if (info.mBigrams != null) { - for (final PendingAttribute attr : info.mBigrams) { - size += FormatSpec.PTNODE_FLAGS_SIZE; - size += BinaryDictEncoderUtils.getByteSize(attr.mAddress); - } - } - return size; - } - - /** - * Write a node array to the stream. - * - * @param destination the stream to write. - * @param infos an array of PtNodeInfo to be written. - * @return the size written, in bytes. - * @throws IOException - */ - static int writeNodes(final OutputStream destination, final PtNodeInfo[] infos) - throws IOException { - int size = getPtNodeCountSize(infos.length); - switch (getPtNodeCountSize(infos.length)) { - case 1: - destination.write((byte)infos.length); - break; - case 2: - final int encodedPtNodeCount = - infos.length | FormatSpec.LARGE_PTNODE_ARRAY_SIZE_FIELD_SIZE_FLAG; - destination.write((byte)(encodedPtNodeCount >> 8)); - destination.write((byte)(encodedPtNodeCount & 0xFF)); - break; - default: - throw new RuntimeException("Invalid node count size."); - } - for (final PtNodeInfo info : infos) size += writePtNode(destination, info); - writeSInt24ToStream(destination, FormatSpec.NO_FORWARD_LINK_ADDRESS); - return size + FormatSpec.FORWARD_LINK_ADDRESS_SIZE; - } - - private static final int HEADER_READING_BUFFER_SIZE = 16384; - /** - * Convenience method to read the header of a binary file. - * - * This is quite resource intensive - don't call when performance is critical. - * - * @param file The file to read. - * @param offset The offset in the file where to start reading the data. - * @param length The length of the data file. - */ - private static FileHeader getDictionaryFileHeader( - final File file, final long offset, final long length) - throws FileNotFoundException, IOException, UnsupportedFormatException { - final byte[] buffer = new byte[HEADER_READING_BUFFER_SIZE]; - final DictDecoder dictDecoder = FormatSpec.getDictDecoder(file, - new DictDecoder.DictionaryBufferFactory() { - @Override - public DictBuffer getDictionaryBuffer(File file) - throws FileNotFoundException, IOException { - final FileInputStream inStream = new FileInputStream(file); - try { - inStream.skip(offset); - inStream.read(buffer); - return new ByteArrayDictBuffer(buffer); - } finally { - inStream.close(); - } - } - } - ); - return dictDecoder.readHeader(); - } - - public static FileHeader getDictionaryFileHeaderOrNull(final File file, final long offset, - final long length) { - try { - final FileHeader header = getDictionaryFileHeader(file, offset, length); - return header; - } catch (UnsupportedFormatException e) { - return null; - } catch (IOException e) { - return null; - } - } - - /** - * Helper method to hide the actual value of the no children address. - */ - public static boolean hasChildrenAddress(final int address) { - return FormatSpec.NO_CHILDREN_ADDRESS != address; - } - - /** - * Helper method to check whether the node is moved. - */ - public static boolean isMovedPtNode(final int flags, final FormatOptions options) { - return options.mSupportsDynamicUpdate - && ((flags & FormatSpec.MASK_CHILDREN_ADDRESS_TYPE) == FormatSpec.FLAG_IS_MOVED); - } - - /** - * Helper method to check whether the dictionary can be updated dynamically. - */ - public static boolean supportsDynamicUpdate(final FormatOptions options) { - return options.mVersion >= FormatSpec.FIRST_VERSION_WITH_DYNAMIC_UPDATE - && options.mSupportsDynamicUpdate; - } - - /** - * Helper method to check whether the node is deleted. - */ - public static boolean isDeletedPtNode(final int flags, final FormatOptions formatOptions) { - return formatOptions.mSupportsDynamicUpdate - && ((flags & FormatSpec.MASK_CHILDREN_ADDRESS_TYPE) == FormatSpec.FLAG_IS_DELETED); - } - - /** - * Compute the binary size of the node count - * @param count the node count - * @return the size of the node count, either 1 or 2 bytes. - */ - public static int getPtNodeCountSize(final int count) { - if (FormatSpec.MAX_PTNODES_FOR_ONE_BYTE_PTNODE_COUNT >= count) { - return 1; - } else if (FormatSpec.MAX_PTNODES_IN_A_PT_NODE_ARRAY >= count) { - return 2; - } else { - throw new RuntimeException("Can't have more than " - + FormatSpec.MAX_PTNODES_IN_A_PT_NODE_ARRAY + " PtNode in a PtNodeArray (found " - + count + ")"); - } - } - - static int getChildrenAddressSize(final int optionFlags, - final FormatOptions formatOptions) { - if (formatOptions.mSupportsDynamicUpdate) return FormatSpec.SIGNED_CHILDREN_ADDRESS_SIZE; - switch (optionFlags & FormatSpec.MASK_CHILDREN_ADDRESS_TYPE) { - case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE: - return 1; - case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES: - return 2; - case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES: - return 3; - case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS: - default: - return 0; - } - } - - /** - * Calculate bigram frequency from compressed value - * - * @param unigramFrequency - * @param bigramFrequency compressed frequency - * @return approximate bigram frequency - */ - public static int reconstructBigramFrequency(final int unigramFrequency, - final int bigramFrequency) { - final float stepSize = (FormatSpec.MAX_TERMINAL_FREQUENCY - unigramFrequency) - / (1.5f + FormatSpec.MAX_BIGRAM_FREQUENCY); - final float resultFreqFloat = unigramFrequency + stepSize * (bigramFrequency + 1.0f); - return (int)resultFreqFloat; - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java deleted file mode 100644 index 3dbeee099..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; -import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; -import com.android.inputmethod.latin.utils.ByteArrayDictBuffer; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.util.ArrayList; -import java.util.TreeMap; - -/** - * An interface of binary dictionary decoders. - */ -public interface DictDecoder { - - /** - * Reads and returns the file header. - */ - public FileHeader readHeader() throws IOException, UnsupportedFormatException; - - /** - * Reads PtNode from nodeAddress. - * @param ptNodePos the position of PtNode. - * @param formatOptions the format options. - * @return PtNodeInfo. - */ - public PtNodeInfo readPtNode(final int ptNodePos, final FormatOptions formatOptions); - - /** - * Reads a buffer and returns the memory representation of the dictionary. - * - * This high-level method takes a buffer and reads its contents, populating a - * FusionDictionary structure. The optional dict argument is an existing dictionary to - * which words from the buffer should be added. If it is null, a new dictionary is created. - * - * @param dict an optional dictionary to add words to, or null. - * @param deleteDictIfBroken a flag indicating whether this method should remove the broken - * dictionary or not. - * @return the created (or merged) dictionary. - */ - @UsedForTesting - public FusionDictionary readDictionaryBinary(final FusionDictionary dict, - final boolean deleteDictIfBroken) - throws FileNotFoundException, IOException, UnsupportedFormatException; - - /** - * Gets the address of the last PtNode of the exact matching word in the dictionary. - * If no match is found, returns NOT_VALID_WORD. - * - * @param word the word we search for. - * @return the address of the terminal node. - * @throws IOException if the file can't be read. - * @throws UnsupportedFormatException if the format of the file is not recognized. - */ - @UsedForTesting - public int getTerminalPosition(final String word) - throws IOException, UnsupportedFormatException; - - /** - * Reads unigrams and bigrams from the binary file. - * Doesn't store a full memory representation of the dictionary. - * - * @param words the map to store the address as a key and the word as a value. - * @param frequencies the map to store the address as a key and the frequency as a value. - * @param bigrams the map to store the address as a key and the list of address as a value. - * @throws IOException if the file can't be read. - * @throws UnsupportedFormatException if the format of the file is not recognized. - */ - @UsedForTesting - public void readUnigramsAndBigramsBinary(final TreeMap<Integer, String> words, - final TreeMap<Integer, Integer> frequencies, - final TreeMap<Integer, ArrayList<PendingAttribute>> bigrams) - throws IOException, UnsupportedFormatException; - - /** - * Sets the position of the buffer to the given value. - * - * @param newPos the new position - */ - public void setPosition(final int newPos); - - /** - * Gets the position of the buffer. - * - * @return the position - */ - public int getPosition(); - - /** - * Reads and returns the PtNode count out of a buffer and forwards the pointer. - */ - public int readPtNodeCount(); - - /** - * Reads the forward link and advances the position. - * - * @return true if this method moves the file pointer, false otherwise. - */ - public boolean readAndFollowForwardLink(); - public boolean hasNextPtNodeArray(); - - /** - * Opens the dictionary file and makes DictBuffer. - */ - @UsedForTesting - public void openDictBuffer() throws FileNotFoundException, IOException; - @UsedForTesting - public boolean isDictBufferOpen(); - - // Constants for DictionaryBufferFactory. - public static final int USE_READONLY_BYTEBUFFER = 0x01000000; - public static final int USE_BYTEARRAY = 0x02000000; - public static final int USE_WRITABLE_BYTEBUFFER = 0x03000000; - public static final int MASK_DICTBUFFER = 0x0F000000; - - public interface DictionaryBufferFactory { - public DictBuffer getDictionaryBuffer(final File file) - throws FileNotFoundException, IOException; - } - - /** - * Creates DictionaryBuffer using a ByteBuffer - * - * This class uses less memory than DictionaryBufferFromByteArrayFactory, - * but doesn't perform as fast. - * When operating on a big dictionary, this class is preferred. - */ - public static final class DictionaryBufferFromReadOnlyByteBufferFactory - implements DictionaryBufferFactory { - @Override - public DictBuffer getDictionaryBuffer(final File file) - throws FileNotFoundException, IOException { - FileInputStream inStream = null; - ByteBuffer buffer = null; - try { - inStream = new FileInputStream(file); - buffer = inStream.getChannel().map(FileChannel.MapMode.READ_ONLY, - 0, file.length()); - } finally { - if (inStream != null) { - inStream.close(); - } - } - if (buffer != null) { - return new BinaryDictDecoderUtils.ByteBufferDictBuffer(buffer); - } - return null; - } - } - - /** - * Creates DictionaryBuffer using a byte array - * - * This class performs faster than other classes, but consumes more memory. - * When operating on a small dictionary, this class is preferred. - */ - public static final class DictionaryBufferFromByteArrayFactory - implements DictionaryBufferFactory { - @Override - public DictBuffer getDictionaryBuffer(final File file) - throws FileNotFoundException, IOException { - FileInputStream inStream = null; - try { - inStream = new FileInputStream(file); - final byte[] array = new byte[(int) file.length()]; - inStream.read(array); - return new ByteArrayDictBuffer(array); - } finally { - if (inStream != null) { - inStream.close(); - } - } - } - } - - /** - * Creates DictionaryBuffer using a writable ByteBuffer and a RandomAccessFile. - * - * This class doesn't perform as fast as other classes, - * but this class is the only option available for destructive operations (insert or delete) - * on a dictionary. - */ - @UsedForTesting - public static final class DictionaryBufferFromWritableByteBufferFactory - implements DictionaryBufferFactory { - @Override - public DictBuffer getDictionaryBuffer(final File file) - throws FileNotFoundException, IOException { - RandomAccessFile raFile = null; - ByteBuffer buffer = null; - try { - raFile = new RandomAccessFile(file, "rw"); - buffer = raFile.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, file.length()); - } finally { - if (raFile != null) { - raFile.close(); - } - } - if (buffer != null) { - return new BinaryDictDecoderUtils.ByteBufferDictBuffer(buffer); - } - return null; - } - } - - public void skipPtNode(final FormatOptions formatOptions); -} diff --git a/java/src/com/android/inputmethod/latin/makedict/DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/DictEncoder.java deleted file mode 100644 index ea5d492d8..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/DictEncoder.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode; - -import java.io.IOException; - -/** - * An interface of binary dictionary encoder. - */ -public interface DictEncoder { - public void writeDictionary(final FusionDictionary dict, final FormatOptions formatOptions) - throws IOException, UnsupportedFormatException; - - public void setPosition(final int position); - public int getPosition(); - public void writePtNodeCount(final int ptNodeCount); - public void writeForwardLinkAddress(final int forwardLinkAddress); - - public void writePtNode(final PtNode ptNode, final int parentPosition, - final FormatOptions formatOptions, final FusionDictionary dict); -} diff --git a/java/src/com/android/inputmethod/latin/makedict/DictUpdater.java b/java/src/com/android/inputmethod/latin/makedict/DictUpdater.java deleted file mode 100644 index c4f7ec91f..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/DictUpdater.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; - -import java.io.IOException; -import java.util.ArrayList; - -/** - * An interface of a binary dictionary updater. - */ -@UsedForTesting -public interface DictUpdater extends DictDecoder { - - /** - * Deletes the word from the binary dictionary. - * - * @param word the word to be deleted. - */ - @UsedForTesting - public void deleteWord(final String word) throws IOException, UnsupportedFormatException; - - /** - * Inserts a word into a binary dictionary. - * - * @param word the word to be inserted. - * @param frequency the frequency of the new word. - * @param bigramStrings bigram list, or null if none. - * @param shortcuts shortcut list, or null if none. - * @param isBlackListEntry whether this should be a blacklist entry. - */ - // TODO: Support batch insertion. - @UsedForTesting - public void insertWord(final String word, final int frequency, - final ArrayList<WeightedString> bigramStrings, - final ArrayList<WeightedString> shortcuts, final boolean isNotAWord, - final boolean isBlackListEntry) throws IOException, UnsupportedFormatException; -} diff --git a/java/src/com/android/inputmethod/latin/makedict/DictionaryHeader.java b/java/src/com/android/inputmethod/latin/makedict/DictionaryHeader.java new file mode 100644 index 000000000..df447fd75 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/makedict/DictionaryHeader.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.makedict; + +import com.android.inputmethod.latin.makedict.FormatSpec.DictionaryOptions; +import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; + +/** + * Class representing dictionary header. + */ +public final class DictionaryHeader { + public final int mBodyOffset; + public final DictionaryOptions mDictionaryOptions; + public final FormatOptions mFormatOptions; + + // Note that these are corresponding definitions in native code in latinime::HeaderPolicy + // and latinime::HeaderReadWriteUtils. + // TODO: Standardize the key names and bump up the format version, taking care not to + // break format version 2 dictionaries. + public static final String DICTIONARY_VERSION_KEY = "version"; + public static final String DICTIONARY_LOCALE_KEY = "locale"; + public static final String DICTIONARY_ID_KEY = "dictionary"; + public static final String DICTIONARY_DESCRIPTION_KEY = "description"; + public static final String DICTIONARY_DATE_KEY = "date"; + public static final String HAS_HISTORICAL_INFO_KEY = "HAS_HISTORICAL_INFO"; + public static final String USES_FORGETTING_CURVE_KEY = "USES_FORGETTING_CURVE"; + public static final String FORGETTING_CURVE_OCCURRENCES_TO_LEVEL_UP_KEY = + "FORGETTING_CURVE_OCCURRENCES_TO_LEVEL_UP"; + public static final String FORGETTING_CURVE_PROBABILITY_VALUES_TABLE_ID_KEY = + "FORGETTING_CURVE_PROBABILITY_VALUES_TABLE_ID"; + public static final String FORGETTING_CURVE_DURATION_TO_LEVEL_DOWN_IN_SECONDS_KEY = + "FORGETTING_CURVE_DURATION_TO_LEVEL_DOWN_IN_SECONDS"; + public static final String MAX_UNIGRAM_COUNT_KEY = "MAX_UNIGRAM_COUNT"; + public static final String MAX_BIGRAM_COUNT_KEY = "MAX_BIGRAM_COUNT"; + public static final String ATTRIBUTE_VALUE_TRUE = "1"; + + public DictionaryHeader(final int headerSize, final DictionaryOptions dictionaryOptions, + final FormatOptions formatOptions) throws UnsupportedFormatException { + mDictionaryOptions = dictionaryOptions; + mFormatOptions = formatOptions; + mBodyOffset = formatOptions.mVersion < FormatSpec.VERSION4 ? headerSize : 0; + if (null == getLocaleString()) { + throw new UnsupportedFormatException("Cannot create a FileHeader without a locale"); + } + if (null == getVersion()) { + throw new UnsupportedFormatException( + "Cannot create a FileHeader without a version"); + } + if (null == getId()) { + throw new UnsupportedFormatException("Cannot create a FileHeader without an ID"); + } + } + + // Helper method to get the locale as a String + public String getLocaleString() { + return mDictionaryOptions.mAttributes.get(DICTIONARY_LOCALE_KEY); + } + + // Helper method to get the version String + public String getVersion() { + return mDictionaryOptions.mAttributes.get(DICTIONARY_VERSION_KEY); + } + + // Helper method to get the dictionary ID as a String + public String getId() { + return mDictionaryOptions.mAttributes.get(DICTIONARY_ID_KEY); + } + + // Helper method to get the description + public String getDescription() { + // TODO: Right now each dictionary file comes with a description in its own language. + // It will display as is no matter the device's locale. It should be internationalized. + return mDictionaryOptions.mAttributes.get(DICTIONARY_DESCRIPTION_KEY); + } +}
\ No newline at end of file diff --git a/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java b/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java deleted file mode 100644 index 28da9ffdd..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java +++ /dev/null @@ -1,492 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; -import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; -import com.android.inputmethod.latin.utils.CollectionUtils; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Arrays; - -/** - * The utility class to help dynamic updates on the binary dictionary. - * - * All the methods in this class are static. - */ -@UsedForTesting -public final class DynamicBinaryDictIOUtils { - private static final boolean DBG = false; - private static final int MAX_JUMPS = 10000; - - private DynamicBinaryDictIOUtils() { - // This utility class is not publicly instantiable. - } - - /* package */ static int markAsDeleted(final int flags) { - return (flags & (~FormatSpec.MASK_CHILDREN_ADDRESS_TYPE)) | FormatSpec.FLAG_IS_DELETED; - } - - /** - * Update a parent address in a PtNode that is referred to by ptNodeOriginAddress. - * - * @param dictUpdater the DictUpdater to write. - * @param ptNodeOriginAddress the address of the PtNode. - * @param newParentAddress the absolute address of the parent. - * @param formatOptions file format options. - */ - private static void updateParentAddress(final Ver3DictUpdater dictUpdater, - final int ptNodeOriginAddress, final int newParentAddress, - final FormatOptions formatOptions) { - final DictBuffer dictBuffer = dictUpdater.getDictBuffer(); - final int originalPosition = dictBuffer.position(); - dictBuffer.position(ptNodeOriginAddress); - if (!formatOptions.mSupportsDynamicUpdate) { - throw new RuntimeException("this file format does not support parent addresses"); - } - final int flags = dictBuffer.readUnsignedByte(); - if (BinaryDictIOUtils.isMovedPtNode(flags, formatOptions)) { - // If the node is moved, the parent address is stored in the destination node. - // We are guaranteed to process the destination node later, so there is no need to - // update anything here. - dictBuffer.position(originalPosition); - return; - } - if (DBG) { - MakedictLog.d("update parent address flags=" + flags + ", " + ptNodeOriginAddress); - } - final int parentOffset = newParentAddress - ptNodeOriginAddress; - BinaryDictIOUtils.writeSInt24ToBuffer(dictBuffer, parentOffset); - dictBuffer.position(originalPosition); - } - - /** - * Update parent addresses in a node array stored at ptNodeOriginAddress. - * - * @param dictUpdater the DictUpdater to be modified. - * @param ptNodeOriginAddress the address of the node array to update. - * @param newParentAddress the address to be written. - * @param formatOptions file format options. - */ - private static void updateParentAddresses(final Ver3DictUpdater dictUpdater, - final int ptNodeOriginAddress, final int newParentAddress, - final FormatOptions formatOptions) { - final int originalPosition = dictUpdater.getPosition(); - dictUpdater.setPosition(ptNodeOriginAddress); - do { - final int count = dictUpdater.readPtNodeCount(); - for (int i = 0; i < count; ++i) { - updateParentAddress(dictUpdater, dictUpdater.getPosition(), newParentAddress, - formatOptions); - dictUpdater.skipPtNode(formatOptions); - } - if (!dictUpdater.readAndFollowForwardLink()) break; - if (dictUpdater.getPosition() == FormatSpec.NO_FORWARD_LINK_ADDRESS) break; - } while (formatOptions.mSupportsDynamicUpdate); - dictUpdater.setPosition(originalPosition); - } - - /** - * Update a children address in a PtNode that is addressed by ptNodeOriginAddress. - * - * @param dictUpdater the DictUpdater to write. - * @param ptNodeOriginAddress the address of the PtNode. - * @param newChildrenAddress the absolute address of the child. - * @param formatOptions file format options. - */ - private static void updateChildrenAddress(final Ver3DictUpdater dictUpdater, - final int ptNodeOriginAddress, final int newChildrenAddress, - final FormatOptions formatOptions) { - final DictBuffer dictBuffer = dictUpdater.getDictBuffer(); - final int originalPosition = dictBuffer.position(); - dictBuffer.position(ptNodeOriginAddress); - final int flags = dictBuffer.readUnsignedByte(); - BinaryDictDecoderUtils.readParentAddress(dictBuffer, formatOptions); - BinaryDictIOUtils.skipString(dictBuffer, (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS) != 0); - if ((flags & FormatSpec.FLAG_IS_TERMINAL) != 0) dictBuffer.readUnsignedByte(); - final int childrenOffset = newChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS - ? FormatSpec.NO_CHILDREN_ADDRESS : newChildrenAddress - dictBuffer.position(); - BinaryDictIOUtils.writeSInt24ToBuffer(dictBuffer, childrenOffset); - dictBuffer.position(originalPosition); - } - - /** - * Helper method to move a PtNode to the tail of the file. - */ - private static int movePtNode(final OutputStream destination, - final Ver3DictUpdater dictUpdater, final PtNodeInfo info, - final int nodeArrayOriginAddress, final int oldNodeAddress, - final FormatOptions formatOptions) throws IOException { - final DictBuffer dictBuffer = dictUpdater.getDictBuffer(); - updateParentAddress(dictUpdater, oldNodeAddress, dictBuffer.limit() + 1, formatOptions); - dictBuffer.position(oldNodeAddress); - final int currentFlags = dictBuffer.readUnsignedByte(); - dictBuffer.position(oldNodeAddress); - dictBuffer.put((byte)(FormatSpec.FLAG_IS_MOVED | (currentFlags - & (~FormatSpec.MASK_MOVE_AND_DELETE_FLAG)))); - int size = FormatSpec.PTNODE_FLAGS_SIZE; - updateForwardLink(dictUpdater, nodeArrayOriginAddress, dictBuffer.limit(), formatOptions); - size += BinaryDictIOUtils.writeNodes(destination, new PtNodeInfo[] { info }); - return size; - } - - @SuppressWarnings("unused") - private static void updateForwardLink(final Ver3DictUpdater dictUpdater, - final int nodeArrayOriginAddress, final int newNodeArrayAddress, - final FormatOptions formatOptions) { - final DictBuffer dictBuffer = dictUpdater.getDictBuffer(); - dictUpdater.setPosition(nodeArrayOriginAddress); - int jumpCount = 0; - while (jumpCount++ < MAX_JUMPS) { - final int count = dictUpdater.readPtNodeCount(); - for (int i = 0; i < count; ++i) { - dictUpdater.readPtNode(dictUpdater.getPosition(), formatOptions); - } - final int forwardLinkAddress = dictBuffer.readUnsignedInt24(); - if (forwardLinkAddress == FormatSpec.NO_FORWARD_LINK_ADDRESS) { - dictBuffer.position(dictBuffer.position() - FormatSpec.FORWARD_LINK_ADDRESS_SIZE); - BinaryDictIOUtils.writeSInt24ToBuffer(dictBuffer, newNodeArrayAddress); - return; - } - dictBuffer.position(forwardLinkAddress); - } - if (DBG && jumpCount >= MAX_JUMPS) { - throw new RuntimeException("too many jumps, probably a bug."); - } - } - - /** - * Move a PtNode that is referred to by oldPtNodeOrigin to the tail of the file, and set the - * children address to the byte after the PtNode. - * - * @param fileEndAddress the address of the tail of the file. - * @param codePoints the characters to put inside the PtNode. - * @param length how many code points to read from codePoints. - * @param flags the flags for this PtNode. - * @param frequency the frequency of this terminal. - * @param parentAddress the address of the parent PtNode of this PtNode. - * @param shortcutTargets the shortcut targets for this PtNode. - * @param bigrams the bigrams for this PtNode. - * @param destination the stream representing the tail of the file. - * @param dictUpdater the DictUpdater. - * @param oldPtNodeArrayOrigin the origin of the old PtNode array this PtNode was a part of. - * @param oldPtNodeOrigin the old origin where this PtNode used to be stored. - * @param formatOptions format options for this dictionary. - * @return the size written, in bytes. - * @throws IOException if the file can't be accessed - */ - private static int movePtNode(final int fileEndAddress, final int[] codePoints, - final int length, final int flags, final int frequency, final int parentAddress, - final ArrayList<WeightedString> shortcutTargets, - final ArrayList<PendingAttribute> bigrams, final OutputStream destination, - final Ver3DictUpdater dictUpdater, final int oldPtNodeArrayOrigin, - final int oldPtNodeOrigin, final FormatOptions formatOptions) throws IOException { - int size = 0; - final int newPtNodeOrigin = fileEndAddress + 1; - final int[] writtenCharacters = Arrays.copyOfRange(codePoints, 0, length); - final PtNodeInfo tmpInfo = new PtNodeInfo(newPtNodeOrigin, -1 /* endAddress */, - flags, writtenCharacters, frequency, parentAddress, FormatSpec.NO_CHILDREN_ADDRESS, - shortcutTargets, bigrams); - size = BinaryDictIOUtils.computePtNodeSize(tmpInfo, formatOptions); - final PtNodeInfo newInfo = new PtNodeInfo(newPtNodeOrigin, newPtNodeOrigin + size, - flags, writtenCharacters, frequency, parentAddress, - fileEndAddress + 1 + size + FormatSpec.FORWARD_LINK_ADDRESS_SIZE, shortcutTargets, - bigrams); - movePtNode(destination, dictUpdater, newInfo, oldPtNodeArrayOrigin, oldPtNodeOrigin, - formatOptions); - return 1 + size + FormatSpec.FORWARD_LINK_ADDRESS_SIZE; - } - - /** - * Converts a list of WeightedString to a list of PendingAttribute. - */ - public static ArrayList<PendingAttribute> resolveBigramPositions(final DictUpdater dictUpdater, - final ArrayList<WeightedString> bigramStrings) - throws IOException, UnsupportedFormatException { - if (bigramStrings == null) return CollectionUtils.newArrayList(); - final ArrayList<PendingAttribute> bigrams = CollectionUtils.newArrayList(); - for (final WeightedString bigram : bigramStrings) { - final int pos = dictUpdater.getTerminalPosition(bigram.mWord); - if (pos == FormatSpec.NOT_VALID_WORD) { - // TODO: figure out what is the correct thing to do here. - } else { - bigrams.add(new PendingAttribute(bigram.mFrequency, pos)); - } - } - return bigrams; - } - - /** - * Insert a word into a binary dictionary. - * - * @param dictUpdater the dict updater. - * @param destination a stream to the underlying file, with the pointer at the end of the file. - * @param word the word to insert. - * @param frequency the frequency of the new word. - * @param bigramStrings bigram list, or null if none. - * @param shortcuts shortcut list, or null if none. - * @param isBlackListEntry whether this should be a blacklist entry. - * @throws IOException if the file can't be accessed. - * @throws UnsupportedFormatException if the existing dictionary is in an unexpected format. - */ - // TODO: Support batch insertion. - // TODO: Remove @UsedForTesting once UserHistoryDictionary is implemented by BinaryDictionary. - @UsedForTesting - public static void insertWord(final Ver3DictUpdater dictUpdater, - final OutputStream destination, final String word, final int frequency, - final ArrayList<WeightedString> bigramStrings, - final ArrayList<WeightedString> shortcuts, final boolean isNotAWord, - final boolean isBlackListEntry) - throws IOException, UnsupportedFormatException { - final ArrayList<PendingAttribute> bigrams = resolveBigramPositions(dictUpdater, - bigramStrings); - final DictBuffer dictBuffer = dictUpdater.getDictBuffer(); - - final boolean isTerminal = true; - final boolean hasBigrams = !bigrams.isEmpty(); - final boolean hasShortcuts = shortcuts != null && !shortcuts.isEmpty(); - - // find the insert position of the word. - if (dictBuffer.position() != 0) dictBuffer.position(0); - final FileHeader fileHeader = dictUpdater.readHeader(); - - int wordPos = 0, address = dictBuffer.position(), nodeOriginAddress = dictBuffer.position(); - final int[] codePoints = FusionDictionary.getCodePoints(word); - final int wordLen = codePoints.length; - - for (int depth = 0; depth < Constants.DICTIONARY_MAX_WORD_LENGTH; ++depth) { - if (wordPos >= wordLen) break; - nodeOriginAddress = dictBuffer.position(); - int nodeParentAddress = -1; - final int ptNodeCount = BinaryDictDecoderUtils.readPtNodeCount(dictBuffer); - boolean foundNextNode = false; - - for (int i = 0; i < ptNodeCount; ++i) { - address = dictBuffer.position(); - final PtNodeInfo currentInfo = dictUpdater.readPtNode(address, - fileHeader.mFormatOptions); - final boolean isMovedNode = BinaryDictIOUtils.isMovedPtNode(currentInfo.mFlags, - fileHeader.mFormatOptions); - if (isMovedNode) continue; - nodeParentAddress = (currentInfo.mParentAddress == FormatSpec.NO_PARENT_ADDRESS) - ? FormatSpec.NO_PARENT_ADDRESS : currentInfo.mParentAddress + address; - boolean matched = true; - for (int p = 0; p < currentInfo.mCharacters.length; ++p) { - if (wordPos + p >= wordLen) { - /* - * splitting - * before - * abcd - ef - * - * insert "abc" - * - * after - * abc - d - ef - */ - final int newNodeAddress = dictBuffer.limit(); - final int flags = BinaryDictEncoderUtils.makePtNodeFlags(p > 1, - isTerminal, 0, hasShortcuts, hasBigrams, false /* isNotAWord */, - false /* isBlackListEntry */, fileHeader.mFormatOptions); - int written = movePtNode(newNodeAddress, currentInfo.mCharacters, p, flags, - frequency, nodeParentAddress, shortcuts, bigrams, destination, - dictUpdater, nodeOriginAddress, address, fileHeader.mFormatOptions); - - final int[] characters2 = Arrays.copyOfRange(currentInfo.mCharacters, p, - currentInfo.mCharacters.length); - if (currentInfo.mChildrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) { - updateParentAddresses(dictUpdater, currentInfo.mChildrenAddress, - newNodeAddress + written + 1, fileHeader.mFormatOptions); - } - final PtNodeInfo newInfo2 = new PtNodeInfo( - newNodeAddress + written + 1, -1 /* endAddress */, - currentInfo.mFlags, characters2, currentInfo.mFrequency, - newNodeAddress + 1, currentInfo.mChildrenAddress, - currentInfo.mShortcutTargets, currentInfo.mBigrams); - BinaryDictIOUtils.writeNodes(destination, new PtNodeInfo[] { newInfo2 }); - return; - } else if (codePoints[wordPos + p] != currentInfo.mCharacters[p]) { - if (p > 0) { - /* - * splitting - * before - * ab - cd - * - * insert "ac" - * - * after - * a - b - cd - * | - * - c - */ - - final int newNodeAddress = dictBuffer.limit(); - final int childrenAddress = currentInfo.mChildrenAddress; - - // move prefix - final int prefixFlags = BinaryDictEncoderUtils.makePtNodeFlags(p > 1, - false /* isTerminal */, 0 /* childrenAddressSize*/, - false /* hasShortcut */, false /* hasBigrams */, - false /* isNotAWord */, false /* isBlackListEntry */, - fileHeader.mFormatOptions); - int written = movePtNode(newNodeAddress, currentInfo.mCharacters, p, - prefixFlags, -1 /* frequency */, nodeParentAddress, null, null, - destination, dictUpdater, nodeOriginAddress, address, - fileHeader.mFormatOptions); - - final int[] suffixCharacters = Arrays.copyOfRange( - currentInfo.mCharacters, p, currentInfo.mCharacters.length); - if (currentInfo.mChildrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) { - updateParentAddresses(dictUpdater, currentInfo.mChildrenAddress, - newNodeAddress + written + 1, fileHeader.mFormatOptions); - } - final int suffixFlags = BinaryDictEncoderUtils.makePtNodeFlags( - suffixCharacters.length > 1, - (currentInfo.mFlags & FormatSpec.FLAG_IS_TERMINAL) != 0, - 0 /* childrenAddressSize */, - (currentInfo.mFlags & FormatSpec.FLAG_HAS_SHORTCUT_TARGETS) - != 0, - (currentInfo.mFlags & FormatSpec.FLAG_HAS_BIGRAMS) != 0, - isNotAWord, isBlackListEntry, fileHeader.mFormatOptions); - final PtNodeInfo suffixInfo = new PtNodeInfo( - newNodeAddress + written + 1, -1 /* endAddress */, suffixFlags, - suffixCharacters, currentInfo.mFrequency, newNodeAddress + 1, - currentInfo.mChildrenAddress, currentInfo.mShortcutTargets, - currentInfo.mBigrams); - written += BinaryDictIOUtils.computePtNodeSize(suffixInfo, - fileHeader.mFormatOptions) + 1; - - final int[] newCharacters = Arrays.copyOfRange(codePoints, wordPos + p, - codePoints.length); - final int flags = BinaryDictEncoderUtils.makePtNodeFlags( - newCharacters.length > 1, isTerminal, - 0 /* childrenAddressSize */, hasShortcuts, hasBigrams, - isNotAWord, isBlackListEntry, fileHeader.mFormatOptions); - final PtNodeInfo newInfo = new PtNodeInfo( - newNodeAddress + written, -1 /* endAddress */, flags, - newCharacters, frequency, newNodeAddress + 1, - FormatSpec.NO_CHILDREN_ADDRESS, shortcuts, bigrams); - BinaryDictIOUtils.writeNodes(destination, - new PtNodeInfo[] { suffixInfo, newInfo }); - return; - } - matched = false; - break; - } - } - - if (matched) { - if (wordPos + currentInfo.mCharacters.length == wordLen) { - // the word exists in the dictionary. - // only update the PtNode. - final int newNodeAddress = dictBuffer.limit(); - final boolean hasMultipleChars = currentInfo.mCharacters.length > 1; - final int flags = BinaryDictEncoderUtils.makePtNodeFlags(hasMultipleChars, - isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams, - isNotAWord, isBlackListEntry, fileHeader.mFormatOptions); - final PtNodeInfo newInfo = new PtNodeInfo(newNodeAddress + 1, - -1 /* endAddress */, flags, currentInfo.mCharacters, frequency, - nodeParentAddress, currentInfo.mChildrenAddress, shortcuts, - bigrams); - movePtNode(destination, dictUpdater, newInfo, nodeOriginAddress, address, - fileHeader.mFormatOptions); - return; - } - wordPos += currentInfo.mCharacters.length; - if (currentInfo.mChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS) { - /* - * found the prefix of the word. - * make new PtNode and link to the PtNode from this PtNode. - * - * before - * ab - cd - * - * insert "abcde" - * - * after - * ab - cd - e - */ - final int newNodeArrayAddress = dictBuffer.limit(); - updateChildrenAddress(dictUpdater, address, newNodeArrayAddress, - fileHeader.mFormatOptions); - final int newNodeAddress = newNodeArrayAddress + 1; - final boolean hasMultipleChars = (wordLen - wordPos) > 1; - final int flags = BinaryDictEncoderUtils.makePtNodeFlags(hasMultipleChars, - isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams, - isNotAWord, isBlackListEntry, fileHeader.mFormatOptions); - final int[] characters = Arrays.copyOfRange(codePoints, wordPos, wordLen); - final PtNodeInfo newInfo = new PtNodeInfo(newNodeAddress, -1, flags, - characters, frequency, address, FormatSpec.NO_CHILDREN_ADDRESS, - shortcuts, bigrams); - BinaryDictIOUtils.writeNodes(destination, new PtNodeInfo[] { newInfo }); - return; - } - dictBuffer.position(currentInfo.mChildrenAddress); - foundNextNode = true; - break; - } - } - - if (foundNextNode) continue; - - // reached the end of the array. - final int linkAddressPosition = dictBuffer.position(); - int nextLink = dictBuffer.readUnsignedInt24(); - if ((nextLink & FormatSpec.MSB24) != 0) { - nextLink = -(nextLink & FormatSpec.SINT24_MAX); - } - if (nextLink == FormatSpec.NO_FORWARD_LINK_ADDRESS) { - /* - * expand this node. - * - * before - * ab - cd - * - * insert "abef" - * - * after - * ab - cd - * | - * - ef - */ - - // change the forward link address. - final int newNodeAddress = dictBuffer.limit(); - dictBuffer.position(linkAddressPosition); - BinaryDictIOUtils.writeSInt24ToBuffer(dictBuffer, newNodeAddress); - - final int[] characters = Arrays.copyOfRange(codePoints, wordPos, wordLen); - final int flags = BinaryDictEncoderUtils.makePtNodeFlags(characters.length > 1, - isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams, - isNotAWord, isBlackListEntry, fileHeader.mFormatOptions); - final PtNodeInfo newInfo = new PtNodeInfo(newNodeAddress + 1, - -1 /* endAddress */, flags, characters, frequency, nodeParentAddress, - FormatSpec.NO_CHILDREN_ADDRESS, shortcuts, bigrams); - BinaryDictIOUtils.writeNodes(destination, new PtNodeInfo[]{ newInfo }); - return; - } else { - depth--; - dictBuffer.position(nextLink); - } - } - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java index b56234f6d..a2ae74b20 100644 --- a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java +++ b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java @@ -18,10 +18,9 @@ package com.android.inputmethod.latin.makedict; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.makedict.DictDecoder.DictionaryBufferFactory; -import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions; -import java.io.File; +import java.util.Date; +import java.util.HashMap; /** * Dictionary File Format Specification. @@ -40,12 +39,8 @@ public final class FormatSpec { * p | not used 3 bits * t | each unigram and bigram entry has a time stamp? * i | 1 bit, 1 = yes, 0 = no : CONTAINS_TIMESTAMP_FLAG - * o | has bigrams ? 1 bit, 1 = yes, 0 = no : CONTAINS_BIGRAMS_FLAG - * n | FRENCH_LIGATURE_PROCESSING_FLAG - * f | supports dynamic updates ? 1 bit, 1 = yes, 0 = no : SUPPORTS_DYNAMIC_UPDATE - * l | GERMAN_UMLAUT_PROCESSING_FLAG - * a | - * gs + * o | + * nflags * * h | * e | size of the file header, 4bytes @@ -82,45 +77,36 @@ public final class FormatSpec { * s * * f | - * o | IF SUPPORTS_DYNAMIC_UPDATE (defined in the file header) - * r | forward link address, 3byte - * w | 1 byte = bbbbbbbb match - * a | case 1xxxxxxx => -((xxxxxxx << 16) + (next byte << 8) + next byte) - * r | otherwise => (xxxxxxx << 16) + (next byte << 8) + next byte - * d | - * linkaddress + * o | forward link address, 3byte + * r | 1 byte = bbbbbbbb match + * w | case 1xxxxxxx => -((xxxxxxx << 16) + (next byte << 8) + next byte) + * a | otherwise => (xxxxxxx << 16) + (next byte << 8) + next byte + * r | + * dlinkaddress */ /* Node (FusionDictionary.PtNode) layout is as follows: - * | IF !SUPPORTS_DYNAMIC_UPDATE - * | addressType xx : mask with MASK_CHILDREN_ADDRESS_TYPE - * | 2 bits, 00 = no children : FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS - * f | 01 = 1 byte : FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE - * l | 10 = 2 bytes : FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES - * a | 11 = 3 bytes : FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES - * g | ELSE - * s | is moved ? 2 bits, 11 = no : FLAG_IS_NOT_MOVED - * | This must be the same as FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES - * | 01 = yes : FLAG_IS_MOVED - * | the new address is stored in the same place as the parent address - * | is deleted? 10 = yes : FLAG_IS_DELETED - * | has several chars ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_MULTIPLE_CHARS - * | has a terminal ? 1 bit, 1 = yes, 0 = no : FLAG_IS_TERMINAL - * | has shortcut targets ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_SHORTCUT_TARGETS + * | is moved ? 2 bits, 11 = no : FLAG_IS_NOT_MOVED + * | This must be the same as FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES + * | 01 = yes : FLAG_IS_MOVED + * f | the new address is stored in the same place as the parent address + * l | is deleted? 10 = yes : FLAG_IS_DELETED + * a | has several chars ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_MULTIPLE_CHARS + * g | has a terminal ? 1 bit, 1 = yes, 0 = no : FLAG_IS_TERMINAL + * s | has shortcut targets ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_SHORTCUT_TARGETS * | has bigrams ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_BIGRAMS * | is not a word ? 1 bit, 1 = yes, 0 = no : FLAG_IS_NOT_A_WORD * | is blacklisted ? 1 bit, 1 = yes, 0 = no : FLAG_IS_BLACKLISTED * * p | - * a | IF SUPPORTS_DYNAMIC_UPDATE (defined in the file header) - * r | parent address, 3byte - * e | 1 byte = bbbbbbbb match - * n | case 1xxxxxxx => -((0xxxxxxx << 16) + (next byte << 8) + next byte) - * t | otherwise => (bbbbbbbb << 16) + (next byte << 8) + next byte - * a | This address is relative to the head of the PtNode. - * d | If the node doesn't have a parent, this field is set to 0. + * a | parent address, 3byte + * r | 1 byte = bbbbbbbb match + * e | case 1xxxxxxx => -((0xxxxxxx << 16) + (next byte << 8) + next byte) + * n | otherwise => (bbbbbbbb << 16) + (next byte << 8) + next byte + * t | This address is relative to the head of the PtNode. + * a | If the node doesn't have a parent, this field is set to 0. * d | - * ress + * dress * * c | IF FLAG_HAS_MULTIPLE_CHARS * h | char, char, char, char n * (1 or 3 bytes) : use PtNodeInfo for i/o helpers @@ -134,23 +120,16 @@ public final class FormatSpec { * e | frequency 1 byte * q | * - * c | IF SUPPORTS_DYNAMIC_UPDATE - * h | children address, 3 bytes - * i | 1 byte = bbbbbbbb match - * l | case 1xxxxxxx => -((0xxxxxxx << 16) + (next byte << 8) + next byte) - * d | otherwise => (bbbbbbbb<<16) + (next byte << 8) + next byte - * r | if this node doesn't have children, this field is set to 0. - * e | (see BinaryDictEncoderUtils#writeVariableSignedAddress) - * n | ELSIF 00 = FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS == addressType - * a | // nothing - * d | ELSIF 01 = FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE == addressType - * d | children address, 1 byte - * r | ELSIF 10 = FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES == addressType - * e | children address, 2 bytes - * s | ELSE // 11 = FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES = addressType - * s | children address, 3 bytes - * | END - * | This address is relative to the position of this field. + * c | + * h | children address, 3 bytes + * i | 1 byte = bbbbbbbb match + * l | case 1xxxxxxx => -((0xxxxxxx << 16) + (next byte << 8) + next byte) + * d | otherwise => (bbbbbbbb<<16) + (next byte << 8) + next byte + * r | if this node doesn't have children, this field is set to 0. + * e | (see BinaryDictEncoderUtils#writeVariableSignedAddress) + * n | This address is relative to the position of this field. + * a | + * ddress * * | IF FLAG_IS_TERMINAL && FLAG_HAS_SHORTCUT_TARGETS * | shortcut string list @@ -199,21 +178,25 @@ public final class FormatSpec { */ public static final int MAGIC_NUMBER = 0x9BC13AFE; - static final int MINIMUM_SUPPORTED_VERSION = 2; - static final int MAXIMUM_SUPPORTED_VERSION = 4; static final int NOT_A_VERSION_NUMBER = -1; static final int FIRST_VERSION_WITH_DYNAMIC_UPDATE = 3; static final int FIRST_VERSION_WITH_TERMINAL_ID = 4; - static final int VERSION3 = 3; - static final int VERSION4 = 4; - // These options need to be the same numeric values as the one in the native reading code. - static final int GERMAN_UMLAUT_PROCESSING_FLAG = 0x1; - // TODO: Make the native reading code read this variable. - static final int SUPPORTS_DYNAMIC_UPDATE = 0x2; - static final int FRENCH_LIGATURE_PROCESSING_FLAG = 0x4; - static final int CONTAINS_BIGRAMS_FLAG = 0x8; - static final int CONTAINS_TIMESTAMP_FLAG = 0x10; + // These MUST have the same values as the relevant constants in format_utils.h. + // From version 4 on, we use version * 100 + revision as a version number. That allows + // us to change the format during development while having testing devices remove + // older files with each upgrade, while still having a readable versioning scheme. + // When we bump up the dictionary format version, we should update + // ExpandableDictionary.needsToMigrateDictionary() and + // ExpandableDictionary.matchesExpectedBinaryDictFormatVersionForThisType(). + public static final int VERSION2 = 2; + // Dictionary version used for testing. + public static final int VERSION4_ONLY_FOR_TESTING = 399; + public static final int VERSION401 = 401; + public static final int VERSION4 = 402; + public static final int VERSION4_DEV = 403; + static final int MINIMUM_SUPPORTED_VERSION = VERSION2; + static final int MAXIMUM_SUPPORTED_VERSION = VERSION4_DEV; // TODO: Make this value adaptative to content data, store it in the header, and // use it in the reading code. @@ -263,29 +246,31 @@ public final class FormatSpec { static final int PTNODE_ATTRIBUTE_MAX_ADDRESS_SIZE = 3; static final int PTNODE_SHORTCUT_LIST_SIZE_SIZE = 2; - // These values are used only by version 4 or later. + // These values are used only by version 4 or later. They MUST match the definitions in + // ver4_dict_constants.cpp. static final String TRIE_FILE_EXTENSION = ".trie"; + public static final String HEADER_FILE_EXTENSION = ".header"; static final String FREQ_FILE_EXTENSION = ".freq"; - static final String UNIGRAM_TIMESTAMP_FILE_EXTENSION = ".timestamp"; // tat = Terminal Address Table static final String TERMINAL_ADDRESS_TABLE_FILE_EXTENSION = ".tat"; static final String BIGRAM_FILE_EXTENSION = ".bigram"; static final String SHORTCUT_FILE_EXTENSION = ".shortcut"; static final String LOOKUP_TABLE_FILE_SUFFIX = "_lookup"; static final String CONTENT_TABLE_FILE_SUFFIX = "_index"; + static final int FLAGS_IN_FREQ_FILE_SIZE = 1; static final int FREQUENCY_AND_FLAGS_SIZE = 2; static final int TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE = 3; static final int UNIGRAM_TIMESTAMP_SIZE = 4; + static final int UNIGRAM_COUNTER_SIZE = 1; + static final int UNIGRAM_LEVEL_SIZE = 1; // With the English main dictionary as of October 2013, the size of bigram address table is - // is 584KB with the block size being 4. - // This is 91% of that of full address table. - static final int BIGRAM_ADDRESS_TABLE_BLOCK_SIZE = 4; - static final int BIGRAM_CONTENT_COUNT = 2; + // is 345KB with the block size being 16. + // This is 54% of that of full address table. + static final int BIGRAM_ADDRESS_TABLE_BLOCK_SIZE = 16; + static final int BIGRAM_CONTENT_COUNT = 1; static final int BIGRAM_FREQ_CONTENT_INDEX = 0; - static final int BIGRAM_TIMESTAMP_CONTENT_INDEX = 1; static final String BIGRAM_FREQ_CONTENT_ID = "_freq"; - static final String BIGRAM_TIMESTAMP_CONTENT_ID = "_timestamp"; static final int BIGRAM_TIMESTAMP_SIZE = 4; static final int BIGRAM_COUNTER_SIZE = 1; static final int BIGRAM_LEVEL_SIZE = 1; @@ -293,7 +278,7 @@ public final class FormatSpec { static final int SHORTCUT_CONTENT_COUNT = 1; static final int SHORTCUT_CONTENT_INDEX = 0; // With the English main dictionary as of October 2013, the size of shortcut address table is - // 29KB with the block size being 64. + // 26KB with the block size being 64. // This is only 4.4% of that of full address table. static final int SHORTCUT_ADDRESS_TABLE_BLOCK_SIZE = 64; static final String SHORTCUT_CONTENT_ID = "_shortcut"; @@ -331,107 +316,56 @@ public final class FormatSpec { */ public static final class FormatOptions { public final int mVersion; - public final boolean mSupportsDynamicUpdate; - public final boolean mHasTerminalId; public final boolean mHasTimestamp; - @UsedForTesting - public FormatOptions(final int version) { - this(version, false); - } @UsedForTesting - public FormatOptions(final int version, final boolean supportsDynamicUpdate) { - this(version, supportsDynamicUpdate, false /* hasTimestamp */); + public FormatOptions(final int version) { + this(version, false /* hasTimestamp */); } - public FormatOptions(final int version, final boolean supportsDynamicUpdate, - final boolean hasTimestamp) { + public FormatOptions(final int version, final boolean hasTimestamp) { mVersion = version; - if (version < FIRST_VERSION_WITH_DYNAMIC_UPDATE && supportsDynamicUpdate) { - throw new RuntimeException("Dynamic updates are only supported with versions " - + FIRST_VERSION_WITH_DYNAMIC_UPDATE + " and ulterior."); - } - mSupportsDynamicUpdate = supportsDynamicUpdate; - mHasTerminalId = (version >= FIRST_VERSION_WITH_TERMINAL_ID); mHasTimestamp = hasTimestamp; } } /** - * Class representing file header. + * Options global to the dictionary. */ - public static final class FileHeader { - public final int mHeaderSize; - public final DictionaryOptions mDictionaryOptions; - public final FormatOptions mFormatOptions; - // Note that these are corresponding definitions in native code in latinime::HeaderPolicy - // and latinime::HeaderReadWriteUtils. - public static final String SUPPORTS_DYNAMIC_UPDATE_ATTRIBUTE = "SUPPORTS_DYNAMIC_UPDATE"; - public static final String USES_FORGETTING_CURVE_ATTRIBUTE = "USES_FORGETTING_CURVE"; - public static final String ATTRIBUTE_VALUE_TRUE = "1"; - - public static final String DICTIONARY_VERSION_ATTRIBUTE = "version"; - public static final String DICTIONARY_LOCALE_ATTRIBUTE = "locale"; - public static final String DICTIONARY_ID_ATTRIBUTE = "dictionary"; - private static final String DICTIONARY_DESCRIPTION_ATTRIBUTE = "description"; - public FileHeader(final int headerSize, final DictionaryOptions dictionaryOptions, - final FormatOptions formatOptions) { - mHeaderSize = headerSize; - mDictionaryOptions = dictionaryOptions; - mFormatOptions = formatOptions; - } - - // Helper method to get the locale as a String - public String getLocaleString() { - return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_LOCALE_ATTRIBUTE); - } - - // Helper method to get the version String - public String getVersion() { - return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_VERSION_ATTRIBUTE); + public static final class DictionaryOptions { + public final HashMap<String, String> mAttributes; + public DictionaryOptions(final HashMap<String, String> attributes) { + mAttributes = attributes; } - - // Helper method to get the dictionary ID as a String - public String getId() { - return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_ID_ATTRIBUTE); - } - - // Helper method to get the description - public String getDescription() { - // TODO: Right now each dictionary file comes with a description in its own language. - // It will display as is no matter the device's locale. It should be internationalized. - return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_DESCRIPTION_ATTRIBUTE); + @Override + public String toString() { // Convenience method + return toString(0, false); } - } - - /** - * Returns new dictionary decoder. - * - * @param dictFile the dictionary file. - * @param bufferType The type of buffer, as one of USE_* in DictDecoder. - * @return new dictionary decoder if the dictionary file exists, otherwise null. - */ - public static DictDecoder getDictDecoder(final File dictFile, final int bufferType) { - if (dictFile.isDirectory()) { - return new Ver4DictDecoder(dictFile, bufferType); - } else if (dictFile.isFile()) { - return new Ver3DictDecoder(dictFile, bufferType); - } - return null; - } - - public static DictDecoder getDictDecoder(final File dictFile, - final DictionaryBufferFactory factory) { - if (dictFile.isDirectory()) { - return new Ver4DictDecoder(dictFile, factory); - } else if (dictFile.isFile()) { - return new Ver3DictDecoder(dictFile, factory); + public String toString(final int indentCount, final boolean plumbing) { + final StringBuilder indent = new StringBuilder(); + if (plumbing) { + indent.append("H:"); + } else { + for (int i = 0; i < indentCount; ++i) { + indent.append(" "); + } + } + final StringBuilder s = new StringBuilder(); + for (final String optionKey : mAttributes.keySet()) { + s.append(indent); + s.append(optionKey); + s.append(" = "); + if ("date".equals(optionKey) && !plumbing) { + // Date needs a number of milliseconds, but the dictionary contains seconds + s.append(new Date( + 1000 * Long.parseLong(mAttributes.get(optionKey))).toString()); + } else { + s.append(mAttributes.get(optionKey)); + } + s.append("\n"); + } + return s.toString(); } - return null; - } - - public static DictDecoder getDictDecoder(final File dictFile) { - return getDictDecoder(dictFile, DictDecoder.USE_READONLY_BYTEBUFFER); } private FormatSpec() { diff --git a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java deleted file mode 100644 index 3bb218bea..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java +++ /dev/null @@ -1,916 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.Constants; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedList; - -/** - * A dictionary that can fusion heads and tails of words for more compression. - */ -@UsedForTesting -public final class FusionDictionary implements Iterable<Word> { - private static final boolean DBG = MakedictLog.DBG; - - private static int CHARACTER_NOT_FOUND_INDEX = -1; - - /** - * A node array of the dictionary, containing several PtNodes. - * - * A PtNodeArray is but an ordered array of PtNodes, which essentially contain all the - * real information. - * This class also contains fields to cache size and address, to help with binary - * generation. - */ - public static final class PtNodeArray { - ArrayList<PtNode> mData; - // To help with binary generation - int mCachedSize = Integer.MIN_VALUE; - // mCachedAddressBefore/AfterUpdate are helpers for binary dictionary generation. They - // always hold the same value except between dictionary address compression, during which - // the update process needs to know about both values at the same time. Updating will - // update the AfterUpdate value, and the code will move them to BeforeUpdate before - // the next update pass. - int mCachedAddressBeforeUpdate = Integer.MIN_VALUE; - int mCachedAddressAfterUpdate = Integer.MIN_VALUE; - int mCachedParentAddress = 0; - - public PtNodeArray() { - mData = new ArrayList<PtNode>(); - } - public PtNodeArray(ArrayList<PtNode> data) { - mData = data; - } - } - - /** - * A string with a frequency. - * - * This represents an "attribute", that is either a bigram or a shortcut. - */ - public static final class WeightedString { - public final String mWord; - public int mFrequency; - public WeightedString(String word, int frequency) { - mWord = word; - mFrequency = frequency; - } - - @Override - public int hashCode() { - return Arrays.hashCode(new Object[] { mWord, mFrequency }); - } - - @Override - public boolean equals(Object o) { - if (o == this) return true; - if (!(o instanceof WeightedString)) return false; - WeightedString w = (WeightedString)o; - return mWord.equals(w.mWord) && mFrequency == w.mFrequency; - } - } - - /** - * PtNode is a group of characters, with a frequency, shortcut targets, bigrams, and children - * (Pt means Patricia Trie). - * - * This is the central class of the in-memory representation. A PtNode is what can - * be seen as a traditional "trie node", except it can hold several characters at the - * same time. A PtNode essentially represents one or several characters in the middle - * of the trie tree; as such, it can be a terminal, and it can have children. - * In this in-memory representation, whether the PtNode is a terminal or not is represented - * in the frequency, where NOT_A_TERMINAL (= -1) means this is not a terminal and any other - * value is the frequency of this terminal. A terminal may have non-null shortcuts and/or - * bigrams, but a non-terminal may not. Moreover, children, if present, are null. - */ - public static final class PtNode { - public static final int NOT_A_TERMINAL = -1; - final int mChars[]; - ArrayList<WeightedString> mShortcutTargets; - ArrayList<WeightedString> mBigrams; - int mFrequency; // NOT_A_TERMINAL == mFrequency indicates this is not a terminal. - int mTerminalId; // NOT_A_TERMINAL == mTerminalId indicates this is not a terminal. - PtNodeArray mChildren; - boolean mIsNotAWord; // Only a shortcut - boolean mIsBlacklistEntry; - // mCachedSize and mCachedAddressBefore/AfterUpdate are helpers for binary dictionary - // generation. Before and After always hold the same value except during dictionary - // address compression, where the update process needs to know about both values at the - // same time. Updating will update the AfterUpdate value, and the code will move them - // to BeforeUpdate before the next update pass. - // The update process does not need two versions of mCachedSize. - int mCachedSize; // The size, in bytes, of this PtNode. - int mCachedAddressBeforeUpdate; // The address of this PtNode (before update) - int mCachedAddressAfterUpdate; // The address of this PtNode (after update) - - public PtNode(final int[] chars, final ArrayList<WeightedString> shortcutTargets, - final ArrayList<WeightedString> bigrams, final int frequency, - final boolean isNotAWord, final boolean isBlacklistEntry) { - mChars = chars; - mFrequency = frequency; - mTerminalId = frequency; - mShortcutTargets = shortcutTargets; - mBigrams = bigrams; - mChildren = null; - mIsNotAWord = isNotAWord; - mIsBlacklistEntry = isBlacklistEntry; - } - - public PtNode(final int[] chars, final ArrayList<WeightedString> shortcutTargets, - final ArrayList<WeightedString> bigrams, final int frequency, - final boolean isNotAWord, final boolean isBlacklistEntry, - final PtNodeArray children) { - mChars = chars; - mFrequency = frequency; - mShortcutTargets = shortcutTargets; - mBigrams = bigrams; - mChildren = children; - mIsNotAWord = isNotAWord; - mIsBlacklistEntry = isBlacklistEntry; - } - - public void addChild(PtNode n) { - if (null == mChildren) { - mChildren = new PtNodeArray(); - } - mChildren.mData.add(n); - } - - public int getTerminalId() { - return mTerminalId; - } - - public boolean isTerminal() { - return NOT_A_TERMINAL != mFrequency; - } - - public int getFrequency() { - return mFrequency; - } - - public boolean getIsNotAWord() { - return mIsNotAWord; - } - - public boolean getIsBlacklistEntry() { - return mIsBlacklistEntry; - } - - public ArrayList<WeightedString> getShortcutTargets() { - // We don't want write permission to escape outside the package, so we return a copy - if (null == mShortcutTargets) return null; - final ArrayList<WeightedString> copyOfShortcutTargets = - new ArrayList<WeightedString>(mShortcutTargets); - return copyOfShortcutTargets; - } - - public ArrayList<WeightedString> getBigrams() { - // We don't want write permission to escape outside the package, so we return a copy - if (null == mBigrams) return null; - final ArrayList<WeightedString> copyOfBigrams = new ArrayList<WeightedString>(mBigrams); - return copyOfBigrams; - } - - public boolean hasSeveralChars() { - assert(mChars.length > 0); - return 1 < mChars.length; - } - - /** - * Adds a word to the bigram list. Updates the frequency if the word already - * exists. - */ - public void addBigram(final String word, final int frequency) { - if (mBigrams == null) { - mBigrams = new ArrayList<WeightedString>(); - } - WeightedString bigram = getBigram(word); - if (bigram != null) { - bigram.mFrequency = frequency; - } else { - bigram = new WeightedString(word, frequency); - mBigrams.add(bigram); - } - } - - /** - * Gets the shortcut target for the given word. Returns null if the word is not in the - * shortcut list. - */ - public WeightedString getShortcut(final String word) { - // TODO: Don't do a linear search - if (mShortcutTargets != null) { - final int size = mShortcutTargets.size(); - for (int i = 0; i < size; ++i) { - WeightedString shortcut = mShortcutTargets.get(i); - if (shortcut.mWord.equals(word)) { - return shortcut; - } - } - } - return null; - } - - /** - * Gets the bigram for the given word. - * Returns null if the word is not in the bigrams list. - */ - public WeightedString getBigram(final String word) { - // TODO: Don't do a linear search - if (mBigrams != null) { - final int size = mBigrams.size(); - for (int i = 0; i < size; ++i) { - WeightedString bigram = mBigrams.get(i); - if (bigram.mWord.equals(word)) { - return bigram; - } - } - } - return null; - } - - /** - * Updates the PtNode with the given properties. Adds the shortcut and bigram lists to - * the existing ones if any. Note: unigram, bigram, and shortcut frequencies are only - * updated if they are higher than the existing ones. - */ - public void update(final int frequency, final ArrayList<WeightedString> shortcutTargets, - final ArrayList<WeightedString> bigrams, - final boolean isNotAWord, final boolean isBlacklistEntry) { - if (frequency > mFrequency) { - mFrequency = frequency; - } - if (shortcutTargets != null) { - if (mShortcutTargets == null) { - mShortcutTargets = shortcutTargets; - } else { - final int size = shortcutTargets.size(); - for (int i = 0; i < size; ++i) { - final WeightedString shortcut = shortcutTargets.get(i); - final WeightedString existingShortcut = getShortcut(shortcut.mWord); - if (existingShortcut == null) { - mShortcutTargets.add(shortcut); - } else if (existingShortcut.mFrequency < shortcut.mFrequency) { - existingShortcut.mFrequency = shortcut.mFrequency; - } - } - } - } - if (bigrams != null) { - if (mBigrams == null) { - mBigrams = bigrams; - } else { - final int size = bigrams.size(); - for (int i = 0; i < size; ++i) { - final WeightedString bigram = bigrams.get(i); - final WeightedString existingBigram = getBigram(bigram.mWord); - if (existingBigram == null) { - mBigrams.add(bigram); - } else if (existingBigram.mFrequency < bigram.mFrequency) { - existingBigram.mFrequency = bigram.mFrequency; - } - } - } - } - mIsNotAWord = isNotAWord; - mIsBlacklistEntry = isBlacklistEntry; - } - } - - /** - * Options global to the dictionary. - */ - public static final class DictionaryOptions { - public final boolean mGermanUmlautProcessing; - public final boolean mFrenchLigatureProcessing; - public final HashMap<String, String> mAttributes; - public DictionaryOptions(final HashMap<String, String> attributes, - final boolean germanUmlautProcessing, final boolean frenchLigatureProcessing) { - mAttributes = attributes; - mGermanUmlautProcessing = germanUmlautProcessing; - mFrenchLigatureProcessing = frenchLigatureProcessing; - } - @Override - public String toString() { // Convenience method - return toString(0, false); - } - public String toString(final int indentCount, final boolean plumbing) { - final StringBuilder indent = new StringBuilder(); - if (plumbing) { - indent.append("H:"); - } else { - for (int i = 0; i < indentCount; ++i) { - indent.append(" "); - } - } - final StringBuilder s = new StringBuilder(); - for (final String optionKey : mAttributes.keySet()) { - s.append(indent); - s.append(optionKey); - s.append(" = "); - if ("date".equals(optionKey) && !plumbing) { - // Date needs a number of milliseconds, but the dictionary contains seconds - s.append(new Date( - 1000 * Long.parseLong(mAttributes.get(optionKey))).toString()); - } else { - s.append(mAttributes.get(optionKey)); - } - s.append("\n"); - } - if (mGermanUmlautProcessing) { - s.append(indent); - s.append("Needs German umlaut processing\n"); - } - if (mFrenchLigatureProcessing) { - s.append(indent); - s.append("Needs French ligature processing\n"); - } - return s.toString(); - } - } - - public final DictionaryOptions mOptions; - public final PtNodeArray mRootNodeArray; - - public FusionDictionary(final PtNodeArray rootNodeArray, final DictionaryOptions options) { - mRootNodeArray = rootNodeArray; - mOptions = options; - } - - public void addOptionAttribute(final String key, final String value) { - mOptions.mAttributes.put(key, value); - } - - /** - * Helper method to convert a String to an int array. - */ - static int[] getCodePoints(final String word) { - // TODO: this is a copy-paste of the old contents of StringUtils.toCodePointArray, - // which is not visible from the makedict package. Factor this code. - final int length = word.length(); - if (length <= 0) return new int[] {}; - final char[] characters = word.toCharArray(); - final int[] codePoints = new int[Character.codePointCount(characters, 0, length)]; - int codePoint = Character.codePointAt(characters, 0); - int dsti = 0; - for (int srci = Character.charCount(codePoint); - srci < length; srci += Character.charCount(codePoint), ++dsti) { - codePoints[dsti] = codePoint; - codePoint = Character.codePointAt(characters, srci); - } - codePoints[dsti] = codePoint; - return codePoints; - } - - /** - * Helper method to add a word as a string. - * - * This method adds a word to the dictionary with the given frequency. Optional - * lists of bigrams and shortcuts can be passed here. For each word inside, - * they will be added to the dictionary as necessary. - * - * @param word the word to add. - * @param frequency the frequency of the word, in the range [0..255]. - * @param shortcutTargets a list of shortcut targets for this word, or null. - * @param isNotAWord true if this should not be considered a word (e.g. shortcut only) - */ - public void add(final String word, final int frequency, - final ArrayList<WeightedString> shortcutTargets, final boolean isNotAWord) { - add(getCodePoints(word), frequency, shortcutTargets, isNotAWord, - false /* isBlacklistEntry */); - } - - /** - * Helper method to add a blacklist entry as a string. - * - * @param word the word to add as a blacklist entry. - * @param shortcutTargets a list of shortcut targets for this word, or null. - * @param isNotAWord true if this is not a word for spellcheking purposes (shortcut only or so) - */ - public void addBlacklistEntry(final String word, - final ArrayList<WeightedString> shortcutTargets, final boolean isNotAWord) { - add(getCodePoints(word), 0, shortcutTargets, isNotAWord, true /* isBlacklistEntry */); - } - - /** - * Sanity check for a PtNode array. - * - * This method checks that all PtNodes in a node array are ordered as expected. - * If they are, nothing happens. If they aren't, an exception is thrown. - */ - private void checkStack(PtNodeArray ptNodeArray) { - ArrayList<PtNode> stack = ptNodeArray.mData; - int lastValue = -1; - for (int i = 0; i < stack.size(); ++i) { - int currentValue = stack.get(i).mChars[0]; - if (currentValue <= lastValue) - throw new RuntimeException("Invalid stack"); - else - lastValue = currentValue; - } - } - - /** - * Helper method to add a new bigram to the dictionary. - * - * @param word1 the previous word of the context - * @param word2 the next word of the context - * @param frequency the bigram frequency - */ - public void setBigram(final String word1, final String word2, final int frequency) { - PtNode ptNode = findWordInTree(mRootNodeArray, word1); - if (ptNode != null) { - final PtNode ptNode2 = findWordInTree(mRootNodeArray, word2); - if (ptNode2 == null) { - add(getCodePoints(word2), 0, null, false /* isNotAWord */, - false /* isBlacklistEntry */); - // The PtNode for the first word may have moved by the above insertion, - // if word1 and word2 share a common stem that happens not to have been - // a cutting point until now. In this case, we need to refresh ptNode. - ptNode = findWordInTree(mRootNodeArray, word1); - } - ptNode.addBigram(word2, frequency); - } else { - throw new RuntimeException("First word of bigram not found"); - } - } - - /** - * Add a word to this dictionary. - * - * The shortcuts, if any, have to be in the dictionary already. If they aren't, - * an exception is thrown. - * - * @param word the word, as an int array. - * @param frequency the frequency of the word, in the range [0..255]. - * @param shortcutTargets an optional list of shortcut targets for this word (null if none). - * @param isNotAWord true if this is not a word for spellcheking purposes (shortcut only or so) - * @param isBlacklistEntry true if this is a blacklisted word, false otherwise - */ - private void add(final int[] word, final int frequency, - final ArrayList<WeightedString> shortcutTargets, - final boolean isNotAWord, final boolean isBlacklistEntry) { - assert(frequency >= 0 && frequency <= 255); - if (word.length >= Constants.DICTIONARY_MAX_WORD_LENGTH) { - MakedictLog.w("Ignoring a word that is too long: word.length = " + word.length); - return; - } - - PtNodeArray currentNodeArray = mRootNodeArray; - int charIndex = 0; - - PtNode currentPtNode = null; - int differentCharIndex = 0; // Set by the loop to the index of the char that differs - int nodeIndex = findIndexOfChar(mRootNodeArray, word[charIndex]); - while (CHARACTER_NOT_FOUND_INDEX != nodeIndex) { - currentPtNode = currentNodeArray.mData.get(nodeIndex); - differentCharIndex = compareCharArrays(currentPtNode.mChars, word, charIndex); - if (ARRAYS_ARE_EQUAL != differentCharIndex - && differentCharIndex < currentPtNode.mChars.length) break; - if (null == currentPtNode.mChildren) break; - charIndex += currentPtNode.mChars.length; - if (charIndex >= word.length) break; - currentNodeArray = currentPtNode.mChildren; - nodeIndex = findIndexOfChar(currentNodeArray, word[charIndex]); - } - - if (CHARACTER_NOT_FOUND_INDEX == nodeIndex) { - // No node at this point to accept the word. Create one. - final int insertionIndex = findInsertionIndex(currentNodeArray, word[charIndex]); - final PtNode newPtNode = new PtNode(Arrays.copyOfRange(word, charIndex, word.length), - shortcutTargets, null /* bigrams */, frequency, isNotAWord, isBlacklistEntry); - currentNodeArray.mData.add(insertionIndex, newPtNode); - if (DBG) checkStack(currentNodeArray); - } else { - // There is a word with a common prefix. - if (differentCharIndex == currentPtNode.mChars.length) { - if (charIndex + differentCharIndex >= word.length) { - // The new word is a prefix of an existing word, but the node on which it - // should end already exists as is. Since the old PtNode was not a terminal, - // make it one by filling in its frequency and other attributes - currentPtNode.update(frequency, shortcutTargets, null, isNotAWord, - isBlacklistEntry); - } else { - // The new word matches the full old word and extends past it. - // We only have to create a new node and add it to the end of this. - final PtNode newNode = new PtNode( - Arrays.copyOfRange(word, charIndex + differentCharIndex, word.length), - shortcutTargets, null /* bigrams */, frequency, isNotAWord, - isBlacklistEntry); - currentPtNode.mChildren = new PtNodeArray(); - currentPtNode.mChildren.mData.add(newNode); - } - } else { - if (0 == differentCharIndex) { - // Exact same word. Update the frequency if higher. This will also add the - // new shortcuts to the existing shortcut list if it already exists. - currentPtNode.update(frequency, shortcutTargets, null, - currentPtNode.mIsNotAWord && isNotAWord, - currentPtNode.mIsBlacklistEntry || isBlacklistEntry); - } else { - // Partial prefix match only. We have to replace the current node with a node - // containing the current prefix and create two new ones for the tails. - PtNodeArray newChildren = new PtNodeArray(); - final PtNode newOldWord = new PtNode( - Arrays.copyOfRange(currentPtNode.mChars, differentCharIndex, - currentPtNode.mChars.length), currentPtNode.mShortcutTargets, - currentPtNode.mBigrams, currentPtNode.mFrequency, - currentPtNode.mIsNotAWord, currentPtNode.mIsBlacklistEntry, - currentPtNode.mChildren); - newChildren.mData.add(newOldWord); - - final PtNode newParent; - if (charIndex + differentCharIndex >= word.length) { - newParent = new PtNode( - Arrays.copyOfRange(currentPtNode.mChars, 0, differentCharIndex), - shortcutTargets, null /* bigrams */, frequency, - isNotAWord, isBlacklistEntry, newChildren); - } else { - newParent = new PtNode( - Arrays.copyOfRange(currentPtNode.mChars, 0, differentCharIndex), - null /* shortcutTargets */, null /* bigrams */, -1, - false /* isNotAWord */, false /* isBlacklistEntry */, newChildren); - final PtNode newWord = new PtNode(Arrays.copyOfRange(word, - charIndex + differentCharIndex, word.length), - shortcutTargets, null /* bigrams */, frequency, - isNotAWord, isBlacklistEntry); - final int addIndex = word[charIndex + differentCharIndex] - > currentPtNode.mChars[differentCharIndex] ? 1 : 0; - newChildren.mData.add(addIndex, newWord); - } - currentNodeArray.mData.set(nodeIndex, newParent); - } - if (DBG) checkStack(currentNodeArray); - } - } - } - - private static int ARRAYS_ARE_EQUAL = 0; - - /** - * Custom comparison of two int arrays taken to contain character codes. - * - * This method compares the two arrays passed as an argument in a lexicographic way, - * with an offset in the dst string. - * This method does NOT test for the first character. It is taken to be equal. - * I repeat: this method starts the comparison at 1 <> dstOffset + 1. - * The index where the strings differ is returned. ARRAYS_ARE_EQUAL = 0 is returned if the - * strings are equal. This works BECAUSE we don't look at the first character. - * - * @param src the left-hand side string of the comparison. - * @param dst the right-hand side string of the comparison. - * @param dstOffset the offset in the right-hand side string. - * @return the index at which the strings differ, or ARRAYS_ARE_EQUAL = 0 if they don't. - */ - private static int compareCharArrays(final int[] src, final int[] dst, int dstOffset) { - // We do NOT test the first char, because we come from a method that already - // tested it. - for (int i = 1; i < src.length; ++i) { - if (dstOffset + i >= dst.length) return i; - if (src[i] != dst[dstOffset + i]) return i; - } - if (dst.length > src.length) return src.length; - return ARRAYS_ARE_EQUAL; - } - - /** - * Helper class that compares and sorts two PtNodes according to their - * first element only. I repeat: ONLY the first element is considered, the rest - * is ignored. - * This comparator imposes orderings that are inconsistent with equals. - */ - static private final class PtNodeComparator implements java.util.Comparator<PtNode> { - @Override - public int compare(PtNode p1, PtNode p2) { - if (p1.mChars[0] == p2.mChars[0]) return 0; - return p1.mChars[0] < p2.mChars[0] ? -1 : 1; - } - } - final static private PtNodeComparator PTNODE_COMPARATOR = new PtNodeComparator(); - - /** - * Finds the insertion index of a character within a node array. - */ - private static int findInsertionIndex(final PtNodeArray nodeArray, int character) { - final ArrayList<PtNode> data = nodeArray.mData; - final PtNode reference = new PtNode(new int[] { character }, - null /* shortcutTargets */, null /* bigrams */, 0, false /* isNotAWord */, - false /* isBlacklistEntry */); - int result = Collections.binarySearch(data, reference, PTNODE_COMPARATOR); - return result >= 0 ? result : -result - 1; - } - - /** - * Find the index of a char in a node array, if it exists. - * - * @param nodeArray the node array to search in. - * @param character the character to search for. - * @return the position of the character if it's there, or CHARACTER_NOT_FOUND_INDEX = -1 else. - */ - private static int findIndexOfChar(final PtNodeArray nodeArray, int character) { - final int insertionIndex = findInsertionIndex(nodeArray, character); - if (nodeArray.mData.size() <= insertionIndex) return CHARACTER_NOT_FOUND_INDEX; - return character == nodeArray.mData.get(insertionIndex).mChars[0] ? insertionIndex - : CHARACTER_NOT_FOUND_INDEX; - } - - /** - * Helper method to find a word in a given branch. - */ - @SuppressWarnings("unused") - public static PtNode findWordInTree(PtNodeArray nodeArray, final String string) { - int index = 0; - final StringBuilder checker = DBG ? new StringBuilder() : null; - final int[] codePoints = getCodePoints(string); - - PtNode currentPtNode; - do { - int indexOfGroup = findIndexOfChar(nodeArray, codePoints[index]); - if (CHARACTER_NOT_FOUND_INDEX == indexOfGroup) return null; - currentPtNode = nodeArray.mData.get(indexOfGroup); - - if (codePoints.length - index < currentPtNode.mChars.length) return null; - int newIndex = index; - while (newIndex < codePoints.length && newIndex - index < currentPtNode.mChars.length) { - if (currentPtNode.mChars[newIndex - index] != codePoints[newIndex]) return null; - newIndex++; - } - index = newIndex; - - if (DBG) { - checker.append(new String(currentPtNode.mChars, 0, currentPtNode.mChars.length)); - } - if (index < codePoints.length) { - nodeArray = currentPtNode.mChildren; - } - } while (null != nodeArray && index < codePoints.length); - - if (index < codePoints.length) return null; - if (!currentPtNode.isTerminal()) return null; - if (DBG && !string.equals(checker.toString())) return null; - return currentPtNode; - } - - /** - * Helper method to find out whether a word is in the dict or not. - */ - public boolean hasWord(final String s) { - if (null == s || "".equals(s)) { - throw new RuntimeException("Can't search for a null or empty string"); - } - return null != findWordInTree(mRootNodeArray, s); - } - - /** - * Recursively count the number of PtNodes in a given branch of the trie. - * - * @param nodeArray the parent node. - * @return the number of PtNodes in all the branch under this node. - */ - public static int countPtNodes(final PtNodeArray nodeArray) { - final int nodeSize = nodeArray.mData.size(); - int size = nodeSize; - for (int i = nodeSize - 1; i >= 0; --i) { - PtNode ptNode = nodeArray.mData.get(i); - if (null != ptNode.mChildren) - size += countPtNodes(ptNode.mChildren); - } - return size; - } - - /** - * Recursively count the number of nodes in a given branch of the trie. - * - * @param nodeArray the node array to count. - * @return the number of nodes in this branch. - */ - public static int countNodeArrays(final PtNodeArray nodeArray) { - int size = 1; - for (int i = nodeArray.mData.size() - 1; i >= 0; --i) { - PtNode ptNode = nodeArray.mData.get(i); - if (null != ptNode.mChildren) - size += countNodeArrays(ptNode.mChildren); - } - return size; - } - - // Recursively find out whether there are any bigrams. - // This can be pretty expensive especially if there aren't any (we return as soon - // as we find one, so it's much cheaper if there are bigrams) - private static boolean hasBigramsInternal(final PtNodeArray nodeArray) { - if (null == nodeArray) return false; - for (int i = nodeArray.mData.size() - 1; i >= 0; --i) { - PtNode ptNode = nodeArray.mData.get(i); - if (null != ptNode.mBigrams) return true; - if (hasBigramsInternal(ptNode.mChildren)) return true; - } - return false; - } - - /** - * Finds out whether there are any bigrams in this dictionary. - * - * @return true if there is any bigram, false otherwise. - */ - // TODO: this is expensive especially for large dictionaries without any bigram. - // The up side is, this is always accurate and correct and uses no memory. We should - // find a more efficient way of doing this, without compromising too much on memory - // and ease of use. - public boolean hasBigrams() { - return hasBigramsInternal(mRootNodeArray); - } - - // Historically, the tails of the words were going to be merged to save space. - // However, that would prevent the code to search for a specific address in log(n) - // time so this was abandoned. - // The code is still of interest as it does add some compression to any dictionary - // that has no need for attributes. Implementations that does not read attributes should be - // able to read a dictionary with merged tails. - // Also, the following code does support frequencies, as in, it will only merges - // tails that share the same frequency. Though it would result in the above loss of - // performance while searching by address, it is still technically possible to merge - // tails that contain attributes, but this code does not take that into account - it does - // not compare attributes and will merge terminals with different attributes regardless. - public void mergeTails() { - MakedictLog.i("Do not merge tails"); - return; - -// MakedictLog.i("Merging PtNodes. Number of PtNodes : " + countPtNodes(root)); -// MakedictLog.i("Number of PtNodes : " + countPtNodes(root)); -// -// final HashMap<String, ArrayList<PtNodeArray>> repository = -// new HashMap<String, ArrayList<PtNodeArray>>(); -// mergeTailsInner(repository, root); -// -// MakedictLog.i("Number of different pseudohashes : " + repository.size()); -// int size = 0; -// for (ArrayList<PtNodeArray> a : repository.values()) { -// size += a.size(); -// } -// MakedictLog.i("Number of nodes after merge : " + (1 + size)); -// MakedictLog.i("Recursively seen nodes : " + countNodes(root)); - } - - // The following methods are used by the deactivated mergeTails() -// private static boolean isEqual(PtNodeArray a, PtNodeArray b) { -// if (null == a && null == b) return true; -// if (null == a || null == b) return false; -// if (a.data.size() != b.data.size()) return false; -// final int size = a.data.size(); -// for (int i = size - 1; i >= 0; --i) { -// PtNode aPtNode = a.data.get(i); -// PtNode bPtNode = b.data.get(i); -// if (aPtNode.frequency != bPtNode.frequency) return false; -// if (aPtNode.alternates == null && bPtNode.alternates != null) return false; -// if (aPtNode.alternates != null && !aPtNode.equals(bPtNode.alternates)) return false; -// if (!Arrays.equals(aPtNode.chars, bPtNode.chars)) return false; -// if (!isEqual(aPtNode.children, bPtNode.children)) return false; -// } -// return true; -// } - -// static private HashMap<String, ArrayList<PtNodeArray>> mergeTailsInner( -// final HashMap<String, ArrayList<PtNodeArray>> map, final PtNodeArray nodeArray) { -// final ArrayList<PtNode> branches = nodeArray.data; -// final int nodeSize = branches.size(); -// for (int i = 0; i < nodeSize; ++i) { -// PtNode ptNode = branches.get(i); -// if (null != ptNode.children) { -// String pseudoHash = getPseudoHash(ptNode.children); -// ArrayList<PtNodeArray> similarList = map.get(pseudoHash); -// if (null == similarList) { -// similarList = new ArrayList<PtNodeArray>(); -// map.put(pseudoHash, similarList); -// } -// boolean merged = false; -// for (PtNodeArray similar : similarList) { -// if (isEqual(ptNode.children, similar)) { -// ptNode.children = similar; -// merged = true; -// break; -// } -// } -// if (!merged) { -// similarList.add(ptNode.children); -// } -// mergeTailsInner(map, ptNode.children); -// } -// } -// return map; -// } - -// private static String getPseudoHash(final PtNodeArray nodeArray) { -// StringBuilder s = new StringBuilder(); -// for (PtNode ptNode : nodeArray.data) { -// s.append(ptNode.frequency); -// for (int ch : ptNode.chars) { -// s.append(Character.toChars(ch)); -// } -// } -// return s.toString(); -// } - - /** - * Iterator to walk through a dictionary. - * - * This is purely for convenience. - */ - public static final class DictionaryIterator implements Iterator<Word> { - private static final class Position { - public Iterator<PtNode> pos; - public int length; - public Position(ArrayList<PtNode> ptNodes) { - pos = ptNodes.iterator(); - length = 0; - } - } - final StringBuilder mCurrentString; - final LinkedList<Position> mPositions; - - public DictionaryIterator(ArrayList<PtNode> ptRoot) { - mCurrentString = new StringBuilder(); - mPositions = new LinkedList<Position>(); - final Position rootPos = new Position(ptRoot); - mPositions.add(rootPos); - } - - @Override - public boolean hasNext() { - for (Position p : mPositions) { - if (p.pos.hasNext()) { - return true; - } - } - return false; - } - - @Override - public Word next() { - Position currentPos = mPositions.getLast(); - mCurrentString.setLength(currentPos.length); - - do { - if (currentPos.pos.hasNext()) { - final PtNode currentPtNode = currentPos.pos.next(); - currentPos.length = mCurrentString.length(); - for (int i : currentPtNode.mChars) { - mCurrentString.append(Character.toChars(i)); - } - if (null != currentPtNode.mChildren) { - currentPos = new Position(currentPtNode.mChildren.mData); - currentPos.length = mCurrentString.length(); - mPositions.addLast(currentPos); - } - if (currentPtNode.mFrequency >= 0) { - return new Word(mCurrentString.toString(), currentPtNode.mFrequency, - currentPtNode.mShortcutTargets, currentPtNode.mBigrams, - currentPtNode.mIsNotAWord, currentPtNode.mIsBlacklistEntry); - } - } else { - mPositions.removeLast(); - currentPos = mPositions.getLast(); - mCurrentString.setLength(mPositions.getLast().length); - } - } while (true); - } - - @Override - public void remove() { - throw new UnsupportedOperationException("Unsupported yet"); - } - - } - - /** - * Method to return an iterator. - * - * This method enables Java's enhanced for loop. With this you can have a FusionDictionary x - * and say : for (Word w : x) {} - */ - @Override - public Iterator<Word> iterator() { - return new DictionaryIterator(mRootNodeArray.mData); - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/MakedictLog.java b/java/src/com/android/inputmethod/latin/makedict/MakedictLog.java deleted file mode 100644 index cf07209d9..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/MakedictLog.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import android.util.Log; - -/** - * Wrapper to redirect log events to the right output medium. - */ -public final class MakedictLog { - public static final boolean DBG = false; - private static final String TAG = MakedictLog.class.getSimpleName(); - - public static void d(String message) { - if (DBG) { - Log.d(TAG, message); - } - } - - public static void i(String message) { - if (DBG) { - Log.i(TAG, message); - } - } - - public static void w(String message) { - Log.w(TAG, message); - } - - public static void e(String message) { - Log.e(TAG, message); - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/PendingAttribute.java b/java/src/com/android/inputmethod/latin/makedict/PendingAttribute.java deleted file mode 100644 index 70e24cc98..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/PendingAttribute.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -/** - * A not-yet-resolved attribute. - * - * An attribute is either a bigram or a shortcut. - * All instances of this class are always immutable. - */ -public final class PendingAttribute { - public final int mFrequency; - public final int mAddress; - public PendingAttribute(final int frequency, final int address) { - mFrequency = frequency; - mAddress = address; - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/ProbabilityInfo.java b/java/src/com/android/inputmethod/latin/makedict/ProbabilityInfo.java new file mode 100644 index 000000000..5fcbb6357 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/makedict/ProbabilityInfo.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.makedict; + +import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.BinaryDictionary; +import com.android.inputmethod.latin.utils.CombinedFormatUtils; + +import java.util.Arrays; + +public final class ProbabilityInfo { + public final int mProbability; + // mTimestamp, mLevel and mCount are historical info. These values are depend on the + // implementation in native code; thus, we must not use them and have any assumptions about + // them except for tests. + public final int mTimestamp; + public final int mLevel; + public final int mCount; + + @UsedForTesting + public static ProbabilityInfo max(final ProbabilityInfo probabilityInfo1, + final ProbabilityInfo probabilityInfo2) { + if (probabilityInfo1 == null) { + return probabilityInfo2; + } + if (probabilityInfo2 == null) { + return probabilityInfo1; + } + if (probabilityInfo1.mProbability > probabilityInfo2.mProbability) { + return probabilityInfo1; + } else { + return probabilityInfo2; + } + } + + public ProbabilityInfo(final int probability) { + this(probability, BinaryDictionary.NOT_A_VALID_TIMESTAMP, 0, 0); + } + + public ProbabilityInfo(final int probability, final int timestamp, final int level, + final int count) { + mProbability = probability; + mTimestamp = timestamp; + mLevel = level; + mCount = count; + } + + public boolean hasHistoricalInfo() { + return mTimestamp != BinaryDictionary.NOT_A_VALID_TIMESTAMP; + } + + @Override + public int hashCode() { + if (hasHistoricalInfo()) { + return Arrays.hashCode(new Object[] { mProbability, mTimestamp, mLevel, mCount }); + } else { + return Arrays.hashCode(new Object[] { mProbability }); + } + } + + @Override + public String toString() { + return CombinedFormatUtils.formatProbabilityInfo(this); + } + + @Override + public boolean equals(Object o) { + if (o == this) return true; + if (!(o instanceof ProbabilityInfo)) return false; + final ProbabilityInfo p = (ProbabilityInfo)o; + if (!hasHistoricalInfo() && !p.hasHistoricalInfo()) { + return mProbability == p.mProbability; + } + return mProbability == p.mProbability && mTimestamp == p.mTimestamp && mLevel == p.mLevel + && mCount == p.mCount; + } +}
\ No newline at end of file diff --git a/java/src/com/android/inputmethod/latin/makedict/PtNodeInfo.java b/java/src/com/android/inputmethod/latin/makedict/PtNodeInfo.java deleted file mode 100644 index 188de7a0f..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/PtNodeInfo.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; - -import java.util.ArrayList; - -/** - * Raw PtNode info straight out of a file. This will contain numbers for addresses. - */ -public final class PtNodeInfo { - - public final int mOriginalAddress; - public final int mEndAddress; - public final int mFlags; - public final int[] mCharacters; - public final int mFrequency; - public final int mChildrenAddress; - public final int mParentAddress; - public final ArrayList<WeightedString> mShortcutTargets; - public final ArrayList<PendingAttribute> mBigrams; - - public PtNodeInfo(final int originalAddress, final int endAddress, final int flags, - final int[] characters, final int frequency, final int parentAddress, - final int childrenAddress, final ArrayList<WeightedString> shortcutTargets, - final ArrayList<PendingAttribute> bigrams) { - mOriginalAddress = originalAddress; - mEndAddress = endAddress; - mFlags = flags; - mCharacters = characters; - mFrequency = frequency; - mParentAddress = parentAddress; - mChildrenAddress = childrenAddress; - mShortcutTargets = shortcutTargets; - mBigrams = bigrams; - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/SparseTable.java b/java/src/com/android/inputmethod/latin/makedict/SparseTable.java deleted file mode 100644 index 7592a0c13..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/SparseTable.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.utils.CollectionUtils; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Collections; - -/** - * SparseTable is an extensible map from integer to integer. - * This holds one value for every mBlockSize keys, so it uses 1/mBlockSize'th of the full index - * memory. - */ -@UsedForTesting -public class SparseTable { - - /** - * mLookupTable is indexed by terminal ID, containing exactly one entry for every mBlockSize - * terminals. - * It contains at index i = j / mBlockSize the index in each ArrayList in mContentsTables where - * the values for terminals with IDs j to j + mBlockSize - 1 are stored as an mBlockSize-sized - * integer array. - */ - private final ArrayList<Integer> mLookupTable; - private final ArrayList<ArrayList<Integer>> mContentTables; - - private final int mBlockSize; - private final int mContentTableCount; - public static final int NOT_EXIST = -1; - public static final int SIZE_OF_INT_IN_BYTES = 4; - - @UsedForTesting - public SparseTable(final int initialCapacity, final int blockSize, - final int contentTableCount) { - mBlockSize = blockSize; - final int lookupTableSize = initialCapacity / mBlockSize - + (initialCapacity % mBlockSize > 0 ? 1 : 0); - mLookupTable = new ArrayList<Integer>(Collections.nCopies(lookupTableSize, NOT_EXIST)); - mContentTableCount = contentTableCount; - mContentTables = CollectionUtils.newArrayList(); - for (int i = 0; i < mContentTableCount; ++i) { - mContentTables.add(new ArrayList<Integer>()); - } - } - - @UsedForTesting - public SparseTable(final ArrayList<Integer> lookupTable, - final ArrayList<ArrayList<Integer>> contentTables, final int blockSize) { - mBlockSize = blockSize; - mContentTableCount = contentTables.size(); - mLookupTable = lookupTable; - mContentTables = contentTables; - } - - /** - * Converts an byte array to an int array considering each set of 4 bytes is an int stored in - * big-endian. - * The length of byteArray must be a multiple of four. - * Otherwise, IndexOutOfBoundsException will be raised. - */ - @UsedForTesting - private static ArrayList<Integer> convertByteArrayToIntegerArray(final byte[] byteArray) { - final ArrayList<Integer> integerArray = new ArrayList<Integer>(byteArray.length / 4); - for (int i = 0; i < byteArray.length; i += 4) { - int value = 0; - for (int j = i; j < i + 4; ++j) { - value <<= 8; - value |= byteArray[j] & 0xFF; - } - integerArray.add(value); - } - return integerArray; - } - - @UsedForTesting - public int get(final int contentTableIndex, final int index) { - if (!contains(index)) { - return NOT_EXIST; - } - return mContentTables.get(contentTableIndex).get( - mLookupTable.get(index / mBlockSize) + (index % mBlockSize)); - } - - @UsedForTesting - public ArrayList<Integer> getAll(final int index) { - final ArrayList<Integer> ret = CollectionUtils.newArrayList(); - for (int i = 0; i < mContentTableCount; ++i) { - ret.add(get(i, index)); - } - return ret; - } - - @UsedForTesting - public void set(final int contentTableIndex, final int index, final int value) { - if (mLookupTable.get(index / mBlockSize) == NOT_EXIST) { - mLookupTable.set(index / mBlockSize, mContentTables.get(contentTableIndex).size()); - for (int i = 0; i < mContentTableCount; ++i) { - for (int j = 0; j < mBlockSize; ++j) { - mContentTables.get(i).add(NOT_EXIST); - } - } - } - mContentTables.get(contentTableIndex).set( - mLookupTable.get(index / mBlockSize) + (index % mBlockSize), value); - } - - public void remove(final int indexOfContent, final int index) { - set(indexOfContent, index, NOT_EXIST); - } - - @UsedForTesting - public int size() { - return mLookupTable.size() * mBlockSize; - } - - @UsedForTesting - /* package */ int getContentTableSize() { - // This class always has at least one content table. - return mContentTables.get(0).size(); - } - - @UsedForTesting - /* package */ int getLookupTableSize() { - return mLookupTable.size(); - } - - public boolean contains(final int index) { - if (index < 0 || index / mBlockSize >= mLookupTable.size() - || mLookupTable.get(index / mBlockSize) == NOT_EXIST) { - return false; - } - return true; - } - - @UsedForTesting - public void write(final OutputStream lookupOutStream, final OutputStream[] contentOutStreams) - throws IOException { - if (contentOutStreams.length != mContentTableCount) { - throw new RuntimeException(contentOutStreams.length + " streams are given, but the" - + " table has " + mContentTableCount + " content tables."); - } - for (final int index : mLookupTable) { - BinaryDictEncoderUtils.writeUIntToStream(lookupOutStream, index, SIZE_OF_INT_IN_BYTES); - } - - for (int i = 0; i < contentOutStreams.length; ++i) { - for (final int data : mContentTables.get(i)) { - BinaryDictEncoderUtils.writeUIntToStream(contentOutStreams[i], data, - SIZE_OF_INT_IN_BYTES); - } - } - } - - @UsedForTesting - public void writeToFiles(final File lookupTableFile, final File[] contentFiles) - throws IOException { - FileOutputStream lookupTableOutStream = null; - final FileOutputStream[] contentTableOutStreams = new FileOutputStream[mContentTableCount]; - try { - lookupTableOutStream = new FileOutputStream(lookupTableFile); - for (int i = 0; i < contentFiles.length; ++i) { - contentTableOutStreams[i] = new FileOutputStream(contentFiles[i]); - } - write(lookupTableOutStream, contentTableOutStreams); - } finally { - if (lookupTableOutStream != null) { - lookupTableOutStream.close(); - } - for (int i = 0; i < contentTableOutStreams.length; ++i) { - if (contentTableOutStreams[i] != null) { - contentTableOutStreams[i].close(); - } - } - } - } - - private static byte[] readFileToByteArray(final File file) throws IOException { - final byte[] contents = new byte[(int) file.length()]; - FileInputStream inStream = null; - try { - inStream = new FileInputStream(file); - inStream.read(contents); - } finally { - if (inStream != null) { - inStream.close(); - } - } - return contents; - } - - @UsedForTesting - public static SparseTable readFromFiles(final File lookupTableFile, final File[] contentFiles, - final int blockSize) throws IOException { - final ArrayList<ArrayList<Integer>> contentTables = - new ArrayList<ArrayList<Integer>>(contentFiles.length); - for (int i = 0; i < contentFiles.length; ++i) { - contentTables.add(convertByteArrayToIntegerArray(readFileToByteArray(contentFiles[i]))); - } - return new SparseTable(convertByteArrayToIntegerArray(readFileToByteArray(lookupTableFile)), - contentTables, blockSize); - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java deleted file mode 100644 index acab4f8a5..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java +++ /dev/null @@ -1,271 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding; -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; -import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; -import com.android.inputmethod.latin.utils.JniUtils; - -import android.util.Log; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; - -/** - * An implementation of DictDecoder for version 3 binary dictionary. - */ -@UsedForTesting -public class Ver3DictDecoder extends AbstractDictDecoder { - private static final String TAG = Ver3DictDecoder.class.getSimpleName(); - - static { - JniUtils.loadNativeLibrary(); - } - - // TODO: implement something sensical instead of just a phony method - private static native int doNothing(); - - protected static class PtNodeReader extends AbstractDictDecoder.PtNodeReader { - private static int readFrequency(final DictBuffer dictBuffer) { - return dictBuffer.readUnsignedByte(); - } - } - - protected final File mDictionaryBinaryFile; - private final DictionaryBufferFactory mBufferFactory; - protected DictBuffer mDictBuffer; - - /* package */ Ver3DictDecoder(final File file, final int factoryFlag) { - mDictionaryBinaryFile = file; - mDictBuffer = null; - - if ((factoryFlag & MASK_DICTBUFFER) == USE_READONLY_BYTEBUFFER) { - mBufferFactory = new DictionaryBufferFromReadOnlyByteBufferFactory(); - } else if ((factoryFlag & MASK_DICTBUFFER) == USE_BYTEARRAY) { - mBufferFactory = new DictionaryBufferFromByteArrayFactory(); - } else if ((factoryFlag & MASK_DICTBUFFER) == USE_WRITABLE_BYTEBUFFER) { - mBufferFactory = new DictionaryBufferFromWritableByteBufferFactory(); - } else { - mBufferFactory = new DictionaryBufferFromReadOnlyByteBufferFactory(); - } - } - - /* package */ Ver3DictDecoder(final File file, final DictionaryBufferFactory factory) { - mDictionaryBinaryFile = file; - mBufferFactory = factory; - } - - @Override - public void openDictBuffer() throws FileNotFoundException, IOException { - mDictBuffer = mBufferFactory.getDictionaryBuffer(mDictionaryBinaryFile); - } - - @Override - public boolean isDictBufferOpen() { - return mDictBuffer != null; - } - - /* package */ DictBuffer getDictBuffer() { - return mDictBuffer; - } - - @UsedForTesting - /* package */ DictBuffer openAndGetDictBuffer() throws FileNotFoundException, IOException { - openDictBuffer(); - return getDictBuffer(); - } - - @Override - public FileHeader readHeader() throws IOException, UnsupportedFormatException { - if (mDictBuffer == null) { - openDictBuffer(); - } - final FileHeader header = super.readHeader(mDictBuffer); - final int version = header.mFormatOptions.mVersion; - if (!(version >= 2 && version <= 3)) { - throw new UnsupportedFormatException("File header has a wrong version : " + version); - } - return header; - } - - // TODO: Make this buffer multi thread safe. - private final int[] mCharacterBuffer = new int[FormatSpec.MAX_WORD_LENGTH]; - @Override - public PtNodeInfo readPtNode(final int ptNodePos, final FormatOptions options) { - int addressPointer = ptNodePos; - final int flags = PtNodeReader.readPtNodeOptionFlags(mDictBuffer); - addressPointer += FormatSpec.PTNODE_FLAGS_SIZE; - - final int parentAddress = PtNodeReader.readParentAddress(mDictBuffer, options); - if (BinaryDictIOUtils.supportsDynamicUpdate(options)) { - addressPointer += FormatSpec.PARENT_ADDRESS_SIZE; - } - - final int characters[]; - if (0 != (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS)) { - int index = 0; - int character = CharEncoding.readChar(mDictBuffer); - addressPointer += CharEncoding.getCharSize(character); - while (FormatSpec.INVALID_CHARACTER != character) { - // FusionDictionary is making sure that the length of the word is smaller than - // MAX_WORD_LENGTH. - // So we'll never write past the end of mCharacterBuffer. - mCharacterBuffer[index++] = character; - character = CharEncoding.readChar(mDictBuffer); - addressPointer += CharEncoding.getCharSize(character); - } - characters = Arrays.copyOfRange(mCharacterBuffer, 0, index); - } else { - final int character = CharEncoding.readChar(mDictBuffer); - addressPointer += CharEncoding.getCharSize(character); - characters = new int[] { character }; - } - final int frequency; - if (0 != (FormatSpec.FLAG_IS_TERMINAL & flags)) { - frequency = PtNodeReader.readFrequency(mDictBuffer); - addressPointer += FormatSpec.PTNODE_FREQUENCY_SIZE; - } else { - frequency = PtNode.NOT_A_TERMINAL; - } - int childrenAddress = PtNodeReader.readChildrenAddress(mDictBuffer, flags, options); - if (childrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) { - childrenAddress += addressPointer; - } - addressPointer += BinaryDictIOUtils.getChildrenAddressSize(flags, options); - final ArrayList<WeightedString> shortcutTargets; - if (0 != (flags & FormatSpec.FLAG_HAS_SHORTCUT_TARGETS)) { - // readShortcut will add shortcuts to shortcutTargets. - shortcutTargets = new ArrayList<WeightedString>(); - addressPointer += PtNodeReader.readShortcut(mDictBuffer, shortcutTargets); - } else { - shortcutTargets = null; - } - - final ArrayList<PendingAttribute> bigrams; - if (0 != (flags & FormatSpec.FLAG_HAS_BIGRAMS)) { - bigrams = new ArrayList<PendingAttribute>(); - addressPointer += PtNodeReader.readBigramAddresses(mDictBuffer, bigrams, - addressPointer); - if (bigrams.size() >= FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) { - throw new RuntimeException("Too many bigrams in a PtNode (" + bigrams.size() - + " but max is " + FormatSpec.MAX_BIGRAMS_IN_A_PTNODE + ")"); - } - } else { - bigrams = null; - } - return new PtNodeInfo(ptNodePos, addressPointer, flags, characters, frequency, - parentAddress, childrenAddress, shortcutTargets, bigrams); - } - - @Override - public FusionDictionary readDictionaryBinary(final FusionDictionary dict, - final boolean deleteDictIfBroken) - throws FileNotFoundException, IOException, UnsupportedFormatException { - if (mDictBuffer == null) { - openDictBuffer(); - } - try { - return BinaryDictDecoderUtils.readDictionaryBinary(this, dict); - } catch (IOException e) { - Log.e(TAG, "The dictionary " + mDictionaryBinaryFile.getName() + " is broken.", e); - if (deleteDictIfBroken && !mDictionaryBinaryFile.delete()) { - Log.e(TAG, "Failed to delete the broken dictionary."); - } - throw e; - } catch (UnsupportedFormatException e) { - Log.e(TAG, "The dictionary " + mDictionaryBinaryFile.getName() + " is broken.", e); - if (deleteDictIfBroken && !mDictionaryBinaryFile.delete()) { - Log.e(TAG, "Failed to delete the broken dictionary."); - } - throw e; - } - } - - @Override - public void setPosition(int newPos) { - mDictBuffer.position(newPos); - } - - @Override - public int getPosition() { - return mDictBuffer.position(); - } - - @Override - public int readPtNodeCount() { - return BinaryDictDecoderUtils.readPtNodeCount(mDictBuffer); - } - - @Override - public boolean readAndFollowForwardLink() { - final int nextAddress = mDictBuffer.readUnsignedInt24(); - if (nextAddress >= 0 && nextAddress < mDictBuffer.limit()) { - mDictBuffer.position(nextAddress); - return true; - } - return false; - } - - @Override - public boolean hasNextPtNodeArray() { - return mDictBuffer.position() != FormatSpec.NO_FORWARD_LINK_ADDRESS; - } - - @Override - public void skipPtNode(final FormatOptions formatOptions) { - final int flags = PtNodeReader.readPtNodeOptionFlags(mDictBuffer); - PtNodeReader.readParentAddress(mDictBuffer, formatOptions); - BinaryDictIOUtils.skipString(mDictBuffer, - (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS) != 0); - PtNodeReader.readChildrenAddress(mDictBuffer, flags, formatOptions); - if ((flags & FormatSpec.FLAG_IS_TERMINAL) != 0) PtNodeReader.readFrequency(mDictBuffer); - if ((flags & FormatSpec.FLAG_HAS_SHORTCUT_TARGETS) != 0) { - final int shortcutsSize = mDictBuffer.readUnsignedShort(); - mDictBuffer.position(mDictBuffer.position() + shortcutsSize - - FormatSpec.PTNODE_SHORTCUT_LIST_SIZE_SIZE); - } - if ((flags & FormatSpec.FLAG_HAS_BIGRAMS) != 0) { - int bigramCount = 0; - while (bigramCount++ < FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) { - final int bigramFlags = mDictBuffer.readUnsignedByte(); - switch (bigramFlags & FormatSpec.MASK_BIGRAM_ATTR_ADDRESS_TYPE) { - case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE: - mDictBuffer.readUnsignedByte(); - break; - case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES: - mDictBuffer.readUnsignedShort(); - break; - case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES: - mDictBuffer.readUnsignedInt24(); - break; - } - if ((bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT) == 0) break; - } - if (bigramCount >= FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) { - throw new RuntimeException("Too many bigrams in a PtNode."); - } - } - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java deleted file mode 100644 index 5da34534e..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding; -import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Iterator; - -/** - * An implementation of DictEncoder for version 3 binary dictionary. - */ -public class Ver3DictEncoder implements DictEncoder { - - private final File mDictFile; - private OutputStream mOutStream; - private byte[] mBuffer; - private int mPosition; - - public Ver3DictEncoder(final File dictFile) { - mDictFile = dictFile; - mOutStream = null; - mBuffer = null; - } - - // This constructor is used only by BinaryDictOffdeviceUtilsTests. - // If you want to use this in the production code, you should consider keeping consistency of - // the interface of Ver3DictDecoder by using factory. - public Ver3DictEncoder(final OutputStream outStream) { - mDictFile = null; - mOutStream = outStream; - } - - private void openStream() throws FileNotFoundException { - mOutStream = new FileOutputStream(mDictFile); - } - - private void close() throws IOException { - if (mOutStream != null) { - mOutStream.close(); - mOutStream = null; - } - } - - @Override - public void writeDictionary(final FusionDictionary dict, final FormatOptions formatOptions) - throws IOException, UnsupportedFormatException { - if (formatOptions.mVersion > FormatSpec.VERSION3) { - throw new UnsupportedFormatException( - "The given format options has wrong version number : " - + formatOptions.mVersion); - } - - if (mOutStream == null) { - openStream(); - } - BinaryDictEncoderUtils.writeDictionaryHeader(mOutStream, dict, formatOptions); - - // Addresses are limited to 3 bytes, but since addresses can be relative to each node - // array, the structure itself is not limited to 16MB. However, if it is over 16MB deciding - // the order of the PtNode arrays becomes a quite complicated problem, because though the - // dictionary itself does not have a size limit, each node array must still be within 16MB - // of all its children and parents. As long as this is ensured, the dictionary file may - // grow to any size. - - // Leave the choice of the optimal node order to the flattenTree function. - MakedictLog.i("Flattening the tree..."); - ArrayList<PtNodeArray> flatNodes = BinaryDictEncoderUtils.flattenTree(dict.mRootNodeArray); - - MakedictLog.i("Computing addresses..."); - BinaryDictEncoderUtils.computeAddresses(dict, flatNodes, formatOptions); - MakedictLog.i("Checking PtNode array..."); - if (MakedictLog.DBG) BinaryDictEncoderUtils.checkFlatPtNodeArrayList(flatNodes); - - // Create a buffer that matches the final dictionary size. - final PtNodeArray lastNodeArray = flatNodes.get(flatNodes.size() - 1); - final int bufferSize = lastNodeArray.mCachedAddressAfterUpdate + lastNodeArray.mCachedSize; - mBuffer = new byte[bufferSize]; - - MakedictLog.i("Writing file..."); - - for (PtNodeArray nodeArray : flatNodes) { - BinaryDictEncoderUtils.writePlacedPtNodeArray(dict, this, nodeArray, formatOptions); - } - if (MakedictLog.DBG) BinaryDictEncoderUtils.showStatistics(flatNodes); - mOutStream.write(mBuffer, 0, mPosition); - - MakedictLog.i("Done"); - close(); - } - - @Override - public void setPosition(final int position) { - if (mBuffer == null || position < 0 || position >= mBuffer.length) return; - mPosition = position; - } - - @Override - public int getPosition() { - return mPosition; - } - - @Override - public void writePtNodeCount(final int ptNodeCount) { - final int countSize = BinaryDictIOUtils.getPtNodeCountSize(ptNodeCount); - if (countSize != 1 && countSize != 2) { - throw new RuntimeException("Strange size from getGroupCountSize : " + countSize); - } - final int encodedPtNodeCount = (countSize == 2) ? - (ptNodeCount | FormatSpec.LARGE_PTNODE_ARRAY_SIZE_FIELD_SIZE_FLAG) : ptNodeCount; - mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, encodedPtNodeCount, - countSize); - } - - private void writePtNodeFlags(final PtNode ptNode, final FormatOptions formatOptions) { - final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, formatOptions); - mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, - BinaryDictEncoderUtils.makePtNodeFlags(ptNode, childrenPos, formatOptions), - FormatSpec.PTNODE_FLAGS_SIZE); - } - - private void writeParentPosition(final int parentPosition, final PtNode ptNode, - final FormatOptions formatOptions) { - if (parentPosition == FormatSpec.NO_PARENT_ADDRESS) { - mPosition = BinaryDictEncoderUtils.writeParentAddress(mBuffer, mPosition, - parentPosition, formatOptions); - } else { - mPosition = BinaryDictEncoderUtils.writeParentAddress(mBuffer, mPosition, - parentPosition - ptNode.mCachedAddressAfterUpdate, formatOptions); - } - } - - private void writeCharacters(final int[] codePoints, final boolean hasSeveralChars) { - mPosition = CharEncoding.writeCharArray(codePoints, mBuffer, mPosition); - if (hasSeveralChars) { - mBuffer[mPosition++] = FormatSpec.PTNODE_CHARACTERS_TERMINATOR; - } - } - - private void writeFrequency(final int frequency) { - if (frequency >= 0) { - mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, frequency, - FormatSpec.PTNODE_FREQUENCY_SIZE); - } - } - - private void writeChildrenPosition(final PtNode ptNode, final FormatOptions formatOptions) { - final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, formatOptions); - if (formatOptions.mSupportsDynamicUpdate) { - mPosition += BinaryDictEncoderUtils.writeSignedChildrenPosition(mBuffer, mPosition, - childrenPos); - } else { - mPosition += BinaryDictEncoderUtils.writeChildrenPosition(mBuffer, mPosition, - childrenPos); - } - } - - /** - * Write a shortcut attributes list to mBuffer. - * - * @param shortcuts the shortcut attributes list. - */ - private void writeShortcuts(final ArrayList<WeightedString> shortcuts) { - if (null == shortcuts || shortcuts.isEmpty()) return; - - final int indexOfShortcutByteSize = mPosition; - mPosition += FormatSpec.PTNODE_SHORTCUT_LIST_SIZE_SIZE; - final Iterator<WeightedString> shortcutIterator = shortcuts.iterator(); - while (shortcutIterator.hasNext()) { - final WeightedString target = shortcutIterator.next(); - final int shortcutFlags = BinaryDictEncoderUtils.makeShortcutFlags( - shortcutIterator.hasNext(), - target.mFrequency); - mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, shortcutFlags, - FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE); - final int shortcutShift = CharEncoding.writeString(mBuffer, mPosition, target.mWord); - mPosition += shortcutShift; - } - final int shortcutByteSize = mPosition - indexOfShortcutByteSize; - if (shortcutByteSize > FormatSpec.MAX_SHORTCUT_LIST_SIZE_IN_A_PTNODE) { - throw new RuntimeException("Shortcut list too large"); - } - BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, indexOfShortcutByteSize, shortcutByteSize, - FormatSpec.PTNODE_SHORTCUT_LIST_SIZE_SIZE); - } - - /** - * Write a bigram attributes list to mBuffer. - * - * @param bigrams the bigram attributes list. - * @param dict the dictionary the node array is a part of (for relative offsets). - */ - private void writeBigrams(final ArrayList<WeightedString> bigrams, - final FusionDictionary dict) { - if (bigrams == null) return; - - final Iterator<WeightedString> bigramIterator = bigrams.iterator(); - while (bigramIterator.hasNext()) { - final WeightedString bigram = bigramIterator.next(); - final PtNode target = - FusionDictionary.findWordInTree(dict.mRootNodeArray, bigram.mWord); - final int addressOfBigram = target.mCachedAddressAfterUpdate; - final int unigramFrequencyForThisWord = target.mFrequency; - final int offset = addressOfBigram - - (mPosition + FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE); - final int bigramFlags = BinaryDictEncoderUtils.makeBigramFlags(bigramIterator.hasNext(), - offset, bigram.mFrequency, unigramFrequencyForThisWord, bigram.mWord); - mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, bigramFlags, - FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE); - mPosition += BinaryDictEncoderUtils.writeChildrenPosition(mBuffer, mPosition, - Math.abs(offset)); - } - } - - @Override - public void writeForwardLinkAddress(final int forwardLinkAddress) { - mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, forwardLinkAddress, - FormatSpec.FORWARD_LINK_ADDRESS_SIZE); - } - - @Override - public void writePtNode(final PtNode ptNode, final int parentPosition, - final FormatOptions formatOptions, final FusionDictionary dict) { - writePtNodeFlags(ptNode, formatOptions); - writeParentPosition(parentPosition, ptNode, formatOptions); - writeCharacters(ptNode.mChars, ptNode.hasSeveralChars()); - writeFrequency(ptNode.mFrequency); - writeChildrenPosition(ptNode, formatOptions); - writeShortcuts(ptNode.mShortcutTargets); - writeBigrams(ptNode.mBigrams, dict); - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver3DictUpdater.java b/java/src/com/android/inputmethod/latin/makedict/Ver3DictUpdater.java deleted file mode 100644 index 07adda625..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/Ver3DictUpdater.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; - -/** - * An implementation of DictUpdater for version 3 binary dictionary. - */ -@UsedForTesting -public class Ver3DictUpdater extends Ver3DictDecoder implements DictUpdater { - private OutputStream mOutStream; - - @UsedForTesting - public Ver3DictUpdater(final File dictFile, final int factoryType) { - // DictUpdater must have an updatable DictBuffer. - super(dictFile, ((factoryType & MASK_DICTBUFFER) == USE_BYTEARRAY) - ? USE_BYTEARRAY : USE_WRITABLE_BYTEBUFFER); - mOutStream = null; - } - - private void openStreamAndBuffer() throws FileNotFoundException, IOException { - super.openDictBuffer(); - mOutStream = new FileOutputStream(mDictionaryBinaryFile, true /* append */); - } - - private void close() throws IOException { - if (mOutStream != null) { - mOutStream.close(); - mOutStream = null; - } - } - - @Override @UsedForTesting - public void deleteWord(final String word) throws IOException, UnsupportedFormatException { - if (mOutStream == null) openStreamAndBuffer(); - mDictBuffer.position(0); - readHeader(); - final int wordPos = getTerminalPosition(word); - if (wordPos != FormatSpec.NOT_VALID_WORD) { - mDictBuffer.position(wordPos); - final int flags = mDictBuffer.readUnsignedByte(); - mDictBuffer.position(wordPos); - mDictBuffer.put((byte) DynamicBinaryDictIOUtils.markAsDeleted(flags)); - } - close(); - } - - @Override @UsedForTesting - public void insertWord(final String word, final int frequency, - final ArrayList<WeightedString> bigramStrings, - final ArrayList<WeightedString> shortcuts, - final boolean isNotAWord, final boolean isBlackListEntry) - throws IOException, UnsupportedFormatException { - if (mOutStream == null) openStreamAndBuffer(); - DynamicBinaryDictIOUtils.insertWord(this, mOutStream, word, frequency, bigramStrings, - shortcuts, isNotAWord, isBlackListEntry); - close(); - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java deleted file mode 100644 index 734223ec2..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java +++ /dev/null @@ -1,343 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding; -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; -import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; -import com.android.inputmethod.latin.utils.CollectionUtils; - -import android.util.Log; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; - -/** - * An implementation of binary dictionary decoder for version 4 binary dictionary. - */ -@UsedForTesting -public class Ver4DictDecoder extends AbstractDictDecoder { - private static final String TAG = Ver4DictDecoder.class.getSimpleName(); - - private static final int FILETYPE_TRIE = 1; - private static final int FILETYPE_FREQUENCY = 2; - private static final int FILETYPE_TERMINAL_ADDRESS_TABLE = 3; - private static final int FILETYPE_BIGRAM_FREQ = 4; - private static final int FILETYPE_SHORTCUT = 5; - - private final File mDictDirectory; - private final DictionaryBufferFactory mBufferFactory; - protected DictBuffer mDictBuffer; - private DictBuffer mFrequencyBuffer; - private DictBuffer mTerminalAddressTableBuffer; - private DictBuffer mBigramBuffer; - private DictBuffer mShortcutBuffer; - private SparseTable mBigramAddressTable; - private SparseTable mShortcutAddressTable; - - @UsedForTesting - /* package */ Ver4DictDecoder(final File dictDirectory, final int factoryFlag) { - mDictDirectory = dictDirectory; - mDictBuffer = mFrequencyBuffer = null; - - if ((factoryFlag & MASK_DICTBUFFER) == USE_READONLY_BYTEBUFFER) { - mBufferFactory = new DictionaryBufferFromReadOnlyByteBufferFactory(); - } else if ((factoryFlag & MASK_DICTBUFFER) == USE_BYTEARRAY) { - mBufferFactory = new DictionaryBufferFromByteArrayFactory(); - } else if ((factoryFlag & MASK_DICTBUFFER) == USE_WRITABLE_BYTEBUFFER) { - mBufferFactory = new DictionaryBufferFromWritableByteBufferFactory(); - } else { - mBufferFactory = new DictionaryBufferFromReadOnlyByteBufferFactory(); - } - } - - @UsedForTesting - /* package */ Ver4DictDecoder(final File dictDirectory, final DictionaryBufferFactory factory) { - mDictDirectory = dictDirectory; - mBufferFactory = factory; - mDictBuffer = mFrequencyBuffer = null; - } - - private File getFile(final int fileType) { - if (fileType == FILETYPE_TRIE) { - return new File(mDictDirectory, - mDictDirectory.getName() + FormatSpec.TRIE_FILE_EXTENSION); - } else if (fileType == FILETYPE_FREQUENCY) { - return new File(mDictDirectory, - mDictDirectory.getName() + FormatSpec.FREQ_FILE_EXTENSION); - } else if (fileType == FILETYPE_TERMINAL_ADDRESS_TABLE) { - return new File(mDictDirectory, - mDictDirectory.getName() + FormatSpec.TERMINAL_ADDRESS_TABLE_FILE_EXTENSION); - } else if (fileType == FILETYPE_BIGRAM_FREQ) { - return new File(mDictDirectory, - mDictDirectory.getName() + FormatSpec.BIGRAM_FILE_EXTENSION - + FormatSpec.BIGRAM_FREQ_CONTENT_ID); - } else if (fileType == FILETYPE_SHORTCUT) { - return new File(mDictDirectory, - mDictDirectory.getName() + FormatSpec.SHORTCUT_FILE_EXTENSION - + FormatSpec.SHORTCUT_CONTENT_ID); - } else { - throw new RuntimeException("Unsupported kind of file : " + fileType); - } - } - - @Override - public void openDictBuffer() throws FileNotFoundException, IOException { - mDictBuffer = mBufferFactory.getDictionaryBuffer(getFile(FILETYPE_TRIE)); - mFrequencyBuffer = mBufferFactory.getDictionaryBuffer(getFile(FILETYPE_FREQUENCY)); - mTerminalAddressTableBuffer = mBufferFactory.getDictionaryBuffer( - getFile(FILETYPE_TERMINAL_ADDRESS_TABLE)); - mBigramBuffer = mBufferFactory.getDictionaryBuffer(getFile(FILETYPE_BIGRAM_FREQ)); - loadBigramAddressSparseTable(); - mShortcutBuffer = mBufferFactory.getDictionaryBuffer(getFile(FILETYPE_SHORTCUT)); - loadShortcutAddressSparseTable(); - } - - @Override - public boolean isDictBufferOpen() { - return mDictBuffer != null; - } - - /* package */ DictBuffer getDictBuffer() { - return mDictBuffer; - } - - @Override - public FileHeader readHeader() throws IOException, UnsupportedFormatException { - if (mDictBuffer == null) { - openDictBuffer(); - } - final FileHeader header = super.readHeader(mDictBuffer); - final int version = header.mFormatOptions.mVersion; - if (version != 4) { - throw new UnsupportedFormatException("File header has a wrong version : " + version); - } - return header; - } - - private void loadBigramAddressSparseTable() throws IOException { - final File lookupIndexFile = new File(mDictDirectory, mDictDirectory.getName() - + FormatSpec.BIGRAM_FILE_EXTENSION + FormatSpec.LOOKUP_TABLE_FILE_SUFFIX); - final File freqsFile = new File(mDictDirectory, mDictDirectory.getName() - + FormatSpec.BIGRAM_FILE_EXTENSION + FormatSpec.CONTENT_TABLE_FILE_SUFFIX - + FormatSpec.BIGRAM_FREQ_CONTENT_ID); - mBigramAddressTable = SparseTable.readFromFiles(lookupIndexFile, new File[] { freqsFile }, - FormatSpec.BIGRAM_ADDRESS_TABLE_BLOCK_SIZE); - } - - // TODO: Let's have something like SparseTableContentsReader in this class. - private void loadShortcutAddressSparseTable() throws IOException { - final File lookupIndexFile = new File(mDictDirectory, mDictDirectory.getName() - + FormatSpec.SHORTCUT_FILE_EXTENSION + FormatSpec.LOOKUP_TABLE_FILE_SUFFIX); - final File contentFile = new File(mDictDirectory, mDictDirectory.getName() - + FormatSpec.SHORTCUT_FILE_EXTENSION + FormatSpec.CONTENT_TABLE_FILE_SUFFIX - + FormatSpec.SHORTCUT_CONTENT_ID); - final File timestampsFile = new File(mDictDirectory, mDictDirectory.getName() - + FormatSpec.SHORTCUT_FILE_EXTENSION + FormatSpec.CONTENT_TABLE_FILE_SUFFIX - + FormatSpec.SHORTCUT_CONTENT_ID); - mShortcutAddressTable = SparseTable.readFromFiles(lookupIndexFile, - new File[] { contentFile, timestampsFile }, - FormatSpec.SHORTCUT_ADDRESS_TABLE_BLOCK_SIZE); - } - - protected static class PtNodeReader extends AbstractDictDecoder.PtNodeReader { - protected static int readFrequency(final DictBuffer frequencyBuffer, final int terminalId) { - frequencyBuffer.position(terminalId * FormatSpec.FREQUENCY_AND_FLAGS_SIZE + 1); - return frequencyBuffer.readUnsignedByte(); - } - - protected static int readTerminalId(final DictBuffer dictBuffer) { - return dictBuffer.readInt(); - } - } - - private ArrayList<WeightedString> readShortcuts(final int terminalId) { - if (mShortcutAddressTable.get(0, terminalId) == SparseTable.NOT_EXIST) return null; - - final ArrayList<WeightedString> ret = CollectionUtils.newArrayList(); - final int posOfShortcuts = mShortcutAddressTable.get(FormatSpec.SHORTCUT_CONTENT_INDEX, - terminalId); - mShortcutBuffer.position(posOfShortcuts); - while (true) { - final int flags = mShortcutBuffer.readUnsignedByte(); - final String word = CharEncoding.readString(mShortcutBuffer); - ret.add(new WeightedString(word, - flags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY)); - if (0 == (flags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT)) break; - } - return ret; - } - - // TODO: Make this buffer thread safe. - // TODO: Support words longer than FormatSpec.MAX_WORD_LENGTH. - private final int[] mCharacterBuffer = new int[FormatSpec.MAX_WORD_LENGTH]; - @Override - public PtNodeInfo readPtNode(int ptNodePos, FormatOptions options) { - int addressPointer = ptNodePos; - final int flags = PtNodeReader.readPtNodeOptionFlags(mDictBuffer); - addressPointer += FormatSpec.PTNODE_FLAGS_SIZE; - - final int parentAddress = PtNodeReader.readParentAddress(mDictBuffer, options); - if (BinaryDictIOUtils.supportsDynamicUpdate(options)) { - addressPointer += FormatSpec.PARENT_ADDRESS_SIZE; - } - - final int characters[]; - if (0 != (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS)) { - int index = 0; - int character = CharEncoding.readChar(mDictBuffer); - addressPointer += CharEncoding.getCharSize(character); - while (FormatSpec.INVALID_CHARACTER != character - && index < FormatSpec.MAX_WORD_LENGTH) { - mCharacterBuffer[index++] = character; - character = CharEncoding.readChar(mDictBuffer); - addressPointer += CharEncoding.getCharSize(character); - } - characters = Arrays.copyOfRange(mCharacterBuffer, 0, index); - } else { - final int character = CharEncoding.readChar(mDictBuffer); - addressPointer += CharEncoding.getCharSize(character); - characters = new int[] { character }; - } - final int terminalId; - if (0 != (FormatSpec.FLAG_IS_TERMINAL & flags)) { - terminalId = PtNodeReader.readTerminalId(mDictBuffer); - addressPointer += FormatSpec.PTNODE_TERMINAL_ID_SIZE; - } else { - terminalId = PtNode.NOT_A_TERMINAL; - } - - final int frequency; - if (0 != (FormatSpec.FLAG_IS_TERMINAL & flags)) { - frequency = PtNodeReader.readFrequency(mFrequencyBuffer, terminalId); - } else { - frequency = PtNode.NOT_A_TERMINAL; - } - int childrenAddress = PtNodeReader.readChildrenAddress(mDictBuffer, flags, options); - if (childrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) { - childrenAddress += addressPointer; - } - addressPointer += BinaryDictIOUtils.getChildrenAddressSize(flags, options); - final ArrayList<WeightedString> shortcutTargets = readShortcuts(terminalId); - - final ArrayList<PendingAttribute> bigrams; - if (0 != (flags & FormatSpec.FLAG_HAS_BIGRAMS)) { - bigrams = new ArrayList<PendingAttribute>(); - final int posOfBigrams = mBigramAddressTable.get(0 /* contentTableIndex */, terminalId); - mBigramBuffer.position(posOfBigrams); - while (bigrams.size() < FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) { - // If bigrams.size() reaches FormatSpec.MAX_BIGRAMS_IN_A_PTNODE, - // remaining bigram entries are ignored. - final int bigramFlags = mBigramBuffer.readUnsignedByte(); - final int targetTerminalId = mBigramBuffer.readUnsignedInt24(); - mTerminalAddressTableBuffer.position( - targetTerminalId * FormatSpec.TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE); - final int targetAddress = mTerminalAddressTableBuffer.readUnsignedInt24(); - bigrams.add(new PendingAttribute( - bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY, - targetAddress)); - if (0 == (bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT)) break; - } - if (bigrams.size() >= FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) { - throw new RuntimeException("Too many bigrams in a PtNode (" + bigrams.size() - + " but max is " + FormatSpec.MAX_BIGRAMS_IN_A_PTNODE + ")"); - } - } else { - bigrams = null; - } - return new PtNodeInfo(ptNodePos, addressPointer, flags, characters, frequency, - parentAddress, childrenAddress, shortcutTargets, bigrams); - } - - private void deleteDictFiles() { - final File[] files = mDictDirectory.listFiles(); - for (int i = 0; i < files.length; ++i) { - files[i].delete(); - } - } - - @Override - public FusionDictionary readDictionaryBinary(final FusionDictionary dict, - final boolean deleteDictIfBroken) - throws FileNotFoundException, IOException, UnsupportedFormatException { - if (mDictBuffer == null) { - openDictBuffer(); - } - try { - return BinaryDictDecoderUtils.readDictionaryBinary(this, dict); - } catch (IOException e) { - Log.e(TAG, "The dictionary " + mDictDirectory.getName() + " is broken.", e); - if (deleteDictIfBroken) { - deleteDictFiles(); - } - throw e; - } catch (UnsupportedFormatException e) { - Log.e(TAG, "The dictionary " + mDictDirectory.getName() + " is broken.", e); - if (deleteDictIfBroken) { - deleteDictFiles(); - } - throw e; - } - } - - @Override - public void setPosition(int newPos) { - mDictBuffer.position(newPos); - } - - @Override - public int getPosition() { - return mDictBuffer.position(); - } - - @Override - public int readPtNodeCount() { - return BinaryDictDecoderUtils.readPtNodeCount(mDictBuffer); - } - - @Override - public boolean readAndFollowForwardLink() { - final int nextAddress = mDictBuffer.readUnsignedInt24(); - if (nextAddress >= 0 && nextAddress < mDictBuffer.limit()) { - mDictBuffer.position(nextAddress); - return true; - } - return false; - } - - @Override - public boolean hasNextPtNodeArray() { - return mDictBuffer.position() != FormatSpec.NO_FORWARD_LINK_ADDRESS; - } - - @Override - public void skipPtNode(final FormatOptions formatOptions) { - final int flags = PtNodeReader.readPtNodeOptionFlags(mDictBuffer); - PtNodeReader.readParentAddress(mDictBuffer, formatOptions); - BinaryDictIOUtils.skipString(mDictBuffer, - (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS) != 0); - if ((flags & FormatSpec.FLAG_IS_TERMINAL) != 0) PtNodeReader.readTerminalId(mDictBuffer); - PtNodeReader.readChildrenAddress(mDictBuffer, flags, formatOptions); - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java deleted file mode 100644 index 8d5b48a9b..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java +++ /dev/null @@ -1,475 +0,0 @@ -/* -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; -import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Iterator; - -/** - * An implementation of DictEncoder for version 4 binary dictionary. - */ -@UsedForTesting -public class Ver4DictEncoder implements DictEncoder { - private final File mDictPlacedDir; - private byte[] mTrieBuf; - private int mTriePos; - private int mHeaderSize; - private OutputStream mTrieOutStream; - private OutputStream mFreqOutStream; - private OutputStream mUnigramTimestampOutStream; - private OutputStream mTerminalAddressTableOutStream; - private File mDictDir; - private String mBaseFilename; - private BigramContentWriter mBigramWriter; - private ShortcutContentWriter mShortcutWriter; - - @UsedForTesting - public Ver4DictEncoder(final File dictPlacedDir) { - mDictPlacedDir = dictPlacedDir; - } - - private interface SparseTableContentWriterInterface { - public void write(final OutputStream outStream) throws IOException; - } - - private static class SparseTableContentWriter { - private final int mContentCount; - private final SparseTable mSparseTable; - private final File mLookupTableFile; - protected final File mBaseDir; - private final File[] mAddressTableFiles; - private final File[] mContentFiles; - protected final OutputStream[] mContentOutStreams; - - public SparseTableContentWriter(final String name, final int initialCapacity, - final int blockSize, final File baseDir, final String[] contentFilenames, - final String[] contentIds) { - if (contentFilenames.length != contentIds.length) { - throw new RuntimeException("The length of contentFilenames and the length of" - + " contentIds are different " + contentFilenames.length + ", " - + contentIds.length); - } - mContentCount = contentFilenames.length; - mSparseTable = new SparseTable(initialCapacity, blockSize, mContentCount); - mLookupTableFile = new File(baseDir, name + FormatSpec.LOOKUP_TABLE_FILE_SUFFIX); - mAddressTableFiles = new File[mContentCount]; - mContentFiles = new File[mContentCount]; - mBaseDir = baseDir; - for (int i = 0; i < mContentCount; ++i) { - mAddressTableFiles[i] = new File(mBaseDir, - name + FormatSpec.CONTENT_TABLE_FILE_SUFFIX + contentIds[i]); - mContentFiles[i] = new File(mBaseDir, contentFilenames[i] + contentIds[i]); - } - mContentOutStreams = new OutputStream[mContentCount]; - } - - public void openStreams() throws FileNotFoundException { - for (int i = 0; i < mContentCount; ++i) { - mContentOutStreams[i] = new FileOutputStream(mContentFiles[i]); - } - } - - protected void write(final int contentIndex, final int index, - final SparseTableContentWriterInterface writer) throws IOException { - mSparseTable.set(contentIndex, index, (int) mContentFiles[contentIndex].length()); - writer.write(mContentOutStreams[contentIndex]); - mContentOutStreams[contentIndex].flush(); - } - - public void closeStreams() throws IOException { - mSparseTable.writeToFiles(mLookupTableFile, mAddressTableFiles); - for (int i = 0; i < mContentCount; ++i) { - mContentOutStreams[i].close(); - } - } - } - - private static class BigramContentWriter extends SparseTableContentWriter { - private final boolean mWriteTimestamp; - - public BigramContentWriter(final String name, final int initialCapacity, - final File baseDir, final boolean writeTimestamp) { - super(name + FormatSpec.BIGRAM_FILE_EXTENSION, initialCapacity, - FormatSpec.BIGRAM_ADDRESS_TABLE_BLOCK_SIZE, baseDir, - getContentFilenames(name, writeTimestamp), getContentIds(writeTimestamp)); - mWriteTimestamp = writeTimestamp; - } - - private static String[] getContentFilenames(final String name, - final boolean writeTimestamp) { - final String[] contentFilenames; - if (writeTimestamp) { - contentFilenames = new String[] { name + FormatSpec.BIGRAM_FILE_EXTENSION, - name + FormatSpec.BIGRAM_FILE_EXTENSION }; - } else { - contentFilenames = new String[] { name + FormatSpec.BIGRAM_FILE_EXTENSION }; - } - return contentFilenames; - } - - private static String[] getContentIds(final boolean writeTimestamp) { - final String[] contentIds; - if (writeTimestamp) { - contentIds = new String[] { FormatSpec.BIGRAM_FREQ_CONTENT_ID, - FormatSpec.BIGRAM_TIMESTAMP_CONTENT_ID }; - } else { - contentIds = new String[] { FormatSpec.BIGRAM_FREQ_CONTENT_ID }; - } - return contentIds; - } - - public void writeBigramsForOneWord(final int terminalId, final int bigramCount, - final Iterator<WeightedString> bigramIterator, final FusionDictionary dict) - throws IOException { - write(FormatSpec.BIGRAM_FREQ_CONTENT_INDEX, terminalId, - new SparseTableContentWriterInterface() { - @Override - public void write(final OutputStream outStream) throws IOException { - writeBigramsForOneWordInternal(outStream, bigramIterator, dict); - }}); - if (mWriteTimestamp) { - write(FormatSpec.BIGRAM_TIMESTAMP_CONTENT_INDEX, terminalId, - new SparseTableContentWriterInterface() { - @Override - public void write(final OutputStream outStream) throws IOException { - initBigramTimestampsCountersAndLevelsForOneWordInternal(outStream, - bigramCount); - }}); - } - } - - private void writeBigramsForOneWordInternal(final OutputStream outStream, - final Iterator<WeightedString> bigramIterator, final FusionDictionary dict) - throws IOException { - while (bigramIterator.hasNext()) { - final WeightedString bigram = bigramIterator.next(); - final PtNode target = - FusionDictionary.findWordInTree(dict.mRootNodeArray, bigram.mWord); - final int unigramFrequencyForThisWord = target.mFrequency; - final int bigramFlags = BinaryDictEncoderUtils.makeBigramFlags( - bigramIterator.hasNext(), 0, bigram.mFrequency, - unigramFrequencyForThisWord, bigram.mWord); - BinaryDictEncoderUtils.writeUIntToStream(outStream, bigramFlags, - FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE); - BinaryDictEncoderUtils.writeUIntToStream(outStream, target.mTerminalId, - FormatSpec.PTNODE_ATTRIBUTE_MAX_ADDRESS_SIZE); - } - } - - private void initBigramTimestampsCountersAndLevelsForOneWordInternal( - final OutputStream outStream, final int bigramCount) throws IOException { - for (int i = 0; i < bigramCount; ++i) { - // TODO: Figure out what initial values should be. - BinaryDictEncoderUtils.writeUIntToStream(outStream, 0 /* value */, - FormatSpec.BIGRAM_TIMESTAMP_SIZE); - BinaryDictEncoderUtils.writeUIntToStream(outStream, 0 /* value */, - FormatSpec.BIGRAM_COUNTER_SIZE); - BinaryDictEncoderUtils.writeUIntToStream(outStream, 0 /* value */, - FormatSpec.BIGRAM_LEVEL_SIZE); - } - } - } - - private static class ShortcutContentWriter extends SparseTableContentWriter { - public ShortcutContentWriter(final String name, final int initialCapacity, - final File baseDir) { - super(name + FormatSpec.SHORTCUT_FILE_EXTENSION, initialCapacity, - FormatSpec.SHORTCUT_ADDRESS_TABLE_BLOCK_SIZE, baseDir, - new String[] { name + FormatSpec.SHORTCUT_FILE_EXTENSION }, - new String[] { FormatSpec.SHORTCUT_CONTENT_ID }); - } - - public void writeShortcutForOneWord(final int terminalId, - final Iterator<WeightedString> shortcutIterator) throws IOException { - write(FormatSpec.SHORTCUT_CONTENT_INDEX, terminalId, - new SparseTableContentWriterInterface() { - @Override - public void write(final OutputStream outStream) throws IOException { - writeShortcutForOneWordInternal(outStream, shortcutIterator); - } - }); - } - - private void writeShortcutForOneWordInternal(final OutputStream outStream, - final Iterator<WeightedString> shortcutIterator) throws IOException { - while (shortcutIterator.hasNext()) { - final WeightedString target = shortcutIterator.next(); - final int shortcutFlags = BinaryDictEncoderUtils.makeShortcutFlags( - shortcutIterator.hasNext(), target.mFrequency); - BinaryDictEncoderUtils.writeUIntToStream(outStream, shortcutFlags, - FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE); - CharEncoding.writeString(outStream, target.mWord); - } - } - } - - private void openStreams(final FormatOptions formatOptions, final DictionaryOptions dictOptions) - throws FileNotFoundException, IOException { - final FileHeader header = new FileHeader(0, dictOptions, formatOptions); - mBaseFilename = header.getId() + "." + header.getVersion(); - mDictDir = new File(mDictPlacedDir, mBaseFilename); - final File trieFile = new File(mDictDir, mBaseFilename + FormatSpec.TRIE_FILE_EXTENSION); - final File freqFile = new File(mDictDir, mBaseFilename + FormatSpec.FREQ_FILE_EXTENSION); - final File timestampFile = new File(mDictDir, - mBaseFilename + FormatSpec.UNIGRAM_TIMESTAMP_FILE_EXTENSION); - final File terminalAddressTableFile = new File(mDictDir, - mBaseFilename + FormatSpec.TERMINAL_ADDRESS_TABLE_FILE_EXTENSION); - if (!mDictDir.isDirectory()) { - if (mDictDir.exists()) mDictDir.delete(); - mDictDir.mkdirs(); - } - mTrieOutStream = new FileOutputStream(trieFile); - mFreqOutStream = new FileOutputStream(freqFile); - mTerminalAddressTableOutStream = new FileOutputStream(terminalAddressTableFile); - if (formatOptions.mHasTimestamp) { - mUnigramTimestampOutStream = new FileOutputStream(timestampFile); - } - } - - private void close() throws IOException { - try { - if (mTrieOutStream != null) { - mTrieOutStream.close(); - } - if (mFreqOutStream != null) { - mFreqOutStream.close(); - } - if (mTerminalAddressTableOutStream != null) { - mTerminalAddressTableOutStream.close(); - } - if (mUnigramTimestampOutStream != null) { - mUnigramTimestampOutStream.close(); - } - } finally { - mTrieOutStream = null; - mFreqOutStream = null; - mTerminalAddressTableOutStream = null; - } - } - - @Override - public void writeDictionary(final FusionDictionary dict, final FormatOptions formatOptions) - throws IOException, UnsupportedFormatException { - if (formatOptions.mVersion != FormatSpec.VERSION4) { - throw new UnsupportedFormatException("File header has a wrong version number : " - + formatOptions.mVersion); - } - if (!mDictPlacedDir.isDirectory()) { - throw new UnsupportedFormatException("Given path is not a directory."); - } - - if (mTrieOutStream == null) { - openStreams(formatOptions, dict.mOptions); - } - - mHeaderSize = BinaryDictEncoderUtils.writeDictionaryHeader(mTrieOutStream, dict, - formatOptions); - - MakedictLog.i("Flattening the tree..."); - ArrayList<PtNodeArray> flatNodes = BinaryDictEncoderUtils.flattenTree(dict.mRootNodeArray); - int terminalCount = 0; - for (final PtNodeArray array : flatNodes) { - for (final PtNode node : array.mData) { - if (node.isTerminal()) node.mTerminalId = terminalCount++; - } - } - - MakedictLog.i("Computing addresses..."); - BinaryDictEncoderUtils.computeAddresses(dict, flatNodes, formatOptions); - if (MakedictLog.DBG) BinaryDictEncoderUtils.checkFlatPtNodeArrayList(flatNodes); - - writeTerminalData(flatNodes, terminalCount); - if (formatOptions.mHasTimestamp) { - initUnigramTimestamps(terminalCount); - } - mBigramWriter = new BigramContentWriter(mBaseFilename, terminalCount, mDictDir, - formatOptions.mHasTimestamp); - writeBigrams(flatNodes, dict); - mShortcutWriter = new ShortcutContentWriter(mBaseFilename, terminalCount, mDictDir); - writeShortcuts(flatNodes); - - final PtNodeArray lastNodeArray = flatNodes.get(flatNodes.size() - 1); - final int bufferSize = lastNodeArray.mCachedAddressAfterUpdate + lastNodeArray.mCachedSize; - mTrieBuf = new byte[bufferSize]; - - MakedictLog.i("Writing file..."); - for (PtNodeArray nodeArray : flatNodes) { - BinaryDictEncoderUtils.writePlacedPtNodeArray(dict, this, nodeArray, formatOptions); - } - if (MakedictLog.DBG) { - BinaryDictEncoderUtils.showStatistics(flatNodes); - MakedictLog.i("has " + terminalCount + " terminals."); - } - mTrieOutStream.write(mTrieBuf); - - MakedictLog.i("Done"); - close(); - } - - @Override - public void setPosition(int position) { - if (mTrieBuf == null || position < 0 || position >- mTrieBuf.length) return; - mTriePos = position; - } - - @Override - public int getPosition() { - return mTriePos; - } - - @Override - public void writePtNodeCount(int ptNodeCount) { - final int countSize = BinaryDictIOUtils.getPtNodeCountSize(ptNodeCount); - // ptNodeCount must fit on one byte or two bytes. - // Please see comments in FormatSpec - if (countSize != 1 && countSize != 2) { - throw new RuntimeException("Strange size from getPtNodeCountSize : " + countSize); - } - final int encodedPtNodeCount = (countSize == 2) ? - (ptNodeCount | FormatSpec.LARGE_PTNODE_ARRAY_SIZE_FIELD_SIZE_FLAG) : ptNodeCount; - mTriePos = BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, mTriePos, encodedPtNodeCount, - countSize); - } - - private void writePtNodeFlags(final PtNode ptNode, final FormatOptions formatOptions) { - final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, formatOptions); - mTriePos = BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, mTriePos, - BinaryDictEncoderUtils.makePtNodeFlags(ptNode, childrenPos, formatOptions), - FormatSpec.PTNODE_FLAGS_SIZE); - } - - private void writeParentPosition(int parentPos, final PtNode ptNode, - final FormatOptions formatOptions) { - if (parentPos != FormatSpec.NO_PARENT_ADDRESS) { - parentPos -= ptNode.mCachedAddressAfterUpdate; - } - mTriePos = BinaryDictEncoderUtils.writeParentAddress(mTrieBuf, mTriePos, parentPos, - formatOptions); - } - - private void writeCharacters(final int[] characters, final boolean hasSeveralChars) { - mTriePos = CharEncoding.writeCharArray(characters, mTrieBuf, mTriePos); - if (hasSeveralChars) { - mTrieBuf[mTriePos++] = FormatSpec.PTNODE_CHARACTERS_TERMINATOR; - } - } - - private void writeTerminalId(final int terminalId) { - mTriePos = BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, mTriePos, terminalId, - FormatSpec.PTNODE_TERMINAL_ID_SIZE); - } - - private void writeChildrenPosition(PtNode ptNode, FormatOptions formatOptions) { - final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, formatOptions); - if (formatOptions.mSupportsDynamicUpdate) { - mTriePos += BinaryDictEncoderUtils.writeSignedChildrenPosition(mTrieBuf, - mTriePos, childrenPos); - } else { - mTriePos += BinaryDictEncoderUtils.writeChildrenPosition(mTrieBuf, - mTriePos, childrenPos); - } - } - - private void writeBigrams(final ArrayList<PtNodeArray> flatNodes, final FusionDictionary dict) - throws IOException { - mBigramWriter.openStreams(); - for (final PtNodeArray nodeArray : flatNodes) { - for (final PtNode ptNode : nodeArray.mData) { - if (ptNode.mBigrams != null) { - mBigramWriter.writeBigramsForOneWord(ptNode.mTerminalId, ptNode.mBigrams.size(), - ptNode.mBigrams.iterator(), dict); - } - } - } - mBigramWriter.closeStreams(); - } - - private void writeShortcuts(final ArrayList<PtNodeArray> flatNodes) throws IOException { - mShortcutWriter.openStreams(); - for (final PtNodeArray nodeArray : flatNodes) { - for (final PtNode ptNode : nodeArray.mData) { - if (ptNode.mShortcutTargets != null && !ptNode.mShortcutTargets.isEmpty()) { - mShortcutWriter.writeShortcutForOneWord(ptNode.mTerminalId, - ptNode.mShortcutTargets.iterator()); - } - } - } - mShortcutWriter.closeStreams(); - } - - @Override - public void writeForwardLinkAddress(int forwardLinkAddress) { - mTriePos = BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, mTriePos, - forwardLinkAddress, FormatSpec.FORWARD_LINK_ADDRESS_SIZE); - } - - @Override - public void writePtNode(final PtNode ptNode, final int parentPosition, - final FormatOptions formatOptions, final FusionDictionary dict) { - writePtNodeFlags(ptNode, formatOptions); - writeParentPosition(parentPosition, ptNode, formatOptions); - writeCharacters(ptNode.mChars, ptNode.hasSeveralChars()); - if (ptNode.isTerminal()) { - writeTerminalId(ptNode.mTerminalId); - } - writeChildrenPosition(ptNode, formatOptions); - } - - private void writeTerminalData(final ArrayList<PtNodeArray> flatNodes, - final int terminalCount) throws IOException { - final byte[] freqBuf = new byte[terminalCount * FormatSpec.FREQUENCY_AND_FLAGS_SIZE]; - final byte[] terminalAddressTableBuf = - new byte[terminalCount * FormatSpec.TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE]; - for (final PtNodeArray nodeArray : flatNodes) { - for (final PtNode ptNode : nodeArray.mData) { - if (ptNode.isTerminal()) { - BinaryDictEncoderUtils.writeUIntToBuffer(freqBuf, - ptNode.mTerminalId * FormatSpec.FREQUENCY_AND_FLAGS_SIZE, - ptNode.mFrequency, FormatSpec.FREQUENCY_AND_FLAGS_SIZE); - BinaryDictEncoderUtils.writeUIntToBuffer(terminalAddressTableBuf, - ptNode.mTerminalId * FormatSpec.TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE, - ptNode.mCachedAddressAfterUpdate + mHeaderSize, - FormatSpec.TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE); - } - } - } - mFreqOutStream.write(freqBuf); - mTerminalAddressTableOutStream.write(terminalAddressTableBuf); - } - - private void initUnigramTimestamps(final int terminalCount) throws IOException { - // Initial value of time stamps for each word is 0. - final byte[] unigramTimestampBuf = - new byte[terminalCount * FormatSpec.UNIGRAM_TIMESTAMP_SIZE]; - mUnigramTimestampOutStream.write(unigramTimestampBuf); - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictUpdater.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictUpdater.java deleted file mode 100644 index 3d8f186ba..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/Ver4DictUpdater.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; - -/** - * An implementation of DictUpdater for version 4 binary dictionary. - */ -@UsedForTesting -public class Ver4DictUpdater extends Ver4DictDecoder implements DictUpdater { - - @UsedForTesting - public Ver4DictUpdater(final File dictDirectory, final int factoryType) { - // DictUpdater must have an updatable DictBuffer. - super(dictDirectory, ((factoryType & MASK_DICTBUFFER) == USE_BYTEARRAY) - ? USE_BYTEARRAY : USE_WRITABLE_BYTEBUFFER); - } - - @Override - public void deleteWord(final String word) throws IOException, UnsupportedFormatException { - if (mDictBuffer == null) openDictBuffer(); - readHeader(); - final int wordPos = getTerminalPosition(word); - if (wordPos != FormatSpec.NOT_VALID_WORD) { - mDictBuffer.position(wordPos); - final int flags = PtNodeReader.readPtNodeOptionFlags(mDictBuffer); - mDictBuffer.position(wordPos); - mDictBuffer.put((byte) DynamicBinaryDictIOUtils.markAsDeleted(flags)); - } - } - - @Override - public void insertWord(final String word, final int frequency, - final ArrayList<WeightedString> bigramStrings, final ArrayList<WeightedString> shortcuts, - final boolean isNotAWord, final boolean isBlackListEntry) - throws IOException, UnsupportedFormatException { - // TODO: Implement this method. - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/WeightedString.java b/java/src/com/android/inputmethod/latin/makedict/WeightedString.java new file mode 100644 index 000000000..f6782df9e --- /dev/null +++ b/java/src/com/android/inputmethod/latin/makedict/WeightedString.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.makedict; + +import com.android.inputmethod.annotations.UsedForTesting; + +import java.util.Arrays; + +/** + * A string with a probability. + * + * This represents an "attribute", that is either a bigram or a shortcut. + */ +public final class WeightedString { + public final String mWord; + public ProbabilityInfo mProbabilityInfo; + + public WeightedString(final String word, final int probability) { + this(word, new ProbabilityInfo(probability)); + } + + public WeightedString(final String word, final ProbabilityInfo probabilityInfo) { + mWord = word; + mProbabilityInfo = probabilityInfo; + } + + @UsedForTesting + public int getProbability() { + return mProbabilityInfo.mProbability; + } + + public void setProbability(final int probability) { + mProbabilityInfo = new ProbabilityInfo(probability); + } + + @Override + public int hashCode() { + return Arrays.hashCode(new Object[] { mWord, mProbabilityInfo}); + } + + @Override + public boolean equals(Object o) { + if (o == this) return true; + if (!(o instanceof WeightedString)) return false; + final WeightedString w = (WeightedString)o; + return mWord.equals(w.mWord) && mProbabilityInfo.equals(w.mProbabilityInfo); + } +}
\ No newline at end of file diff --git a/java/src/com/android/inputmethod/latin/makedict/Word.java b/java/src/com/android/inputmethod/latin/makedict/Word.java deleted file mode 100644 index 0eabb7bf3..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/Word.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; - -import java.util.ArrayList; -import java.util.Arrays; - -/** - * Utility class for a word with a frequency. - * - * This is chiefly used to iterate a dictionary. - */ -public final class Word implements Comparable<Word> { - public final String mWord; - public final int mFrequency; - public final ArrayList<WeightedString> mShortcutTargets; - public final ArrayList<WeightedString> mBigrams; - public final boolean mIsNotAWord; - public final boolean mIsBlacklistEntry; - - private int mHashCode = 0; - - public Word(final String word, final int frequency, - final ArrayList<WeightedString> shortcutTargets, - final ArrayList<WeightedString> bigrams, - final boolean isNotAWord, final boolean isBlacklistEntry) { - mWord = word; - mFrequency = frequency; - mShortcutTargets = shortcutTargets; - mBigrams = bigrams; - mIsNotAWord = isNotAWord; - mIsBlacklistEntry = isBlacklistEntry; - } - - private static int computeHashCode(Word word) { - return Arrays.hashCode(new Object[] { - word.mWord, - word.mFrequency, - word.mShortcutTargets.hashCode(), - word.mBigrams.hashCode(), - word.mIsNotAWord, - word.mIsBlacklistEntry - }); - } - - /** - * Three-way comparison. - * - * A Word x is greater than a word y if x has a higher frequency. If they have the same - * frequency, they are sorted in lexicographic order. - */ - @Override - public int compareTo(Word w) { - if (mFrequency < w.mFrequency) return 1; - if (mFrequency > w.mFrequency) return -1; - return mWord.compareTo(w.mWord); - } - - /** - * Equality test. - * - * Words are equal if they have the same frequency, the same spellings, and the same - * attributes. - */ - @Override - public boolean equals(Object o) { - if (o == this) return true; - if (!(o instanceof Word)) return false; - Word w = (Word)o; - return mFrequency == w.mFrequency && mWord.equals(w.mWord) - && mShortcutTargets.equals(w.mShortcutTargets) - && mBigrams.equals(w.mBigrams) - && mIsNotAWord == w.mIsNotAWord - && mIsBlacklistEntry == w.mIsBlacklistEntry; - } - - @Override - public int hashCode() { - if (mHashCode == 0) { - mHashCode = computeHashCode(this); - } - return mHashCode; - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/WordProperty.java b/java/src/com/android/inputmethod/latin/makedict/WordProperty.java new file mode 100644 index 000000000..cd78e2235 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/makedict/WordProperty.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.makedict; + +import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.BinaryDictionary; +import com.android.inputmethod.latin.utils.CombinedFormatUtils; +import com.android.inputmethod.latin.utils.StringUtils; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * Utility class for a word with a probability. + * + * This is chiefly used to iterate a dictionary. + */ +public final class WordProperty implements Comparable<WordProperty> { + public final String mWord; + public final ProbabilityInfo mProbabilityInfo; + public final ArrayList<WeightedString> mShortcutTargets; + public final ArrayList<WeightedString> mBigrams; + // TODO: Support mIsBeginningOfSentence. + public final boolean mIsBeginningOfSentence; + public final boolean mIsNotAWord; + public final boolean mIsBlacklistEntry; + public final boolean mHasShortcuts; + public final boolean mHasBigrams; + + private int mHashCode = 0; + + @UsedForTesting + public WordProperty(final String word, final ProbabilityInfo probabilityInfo, + final ArrayList<WeightedString> shortcutTargets, + final ArrayList<WeightedString> bigrams, + final boolean isNotAWord, final boolean isBlacklistEntry) { + mWord = word; + mProbabilityInfo = probabilityInfo; + mShortcutTargets = shortcutTargets; + mBigrams = bigrams; + mIsBeginningOfSentence = false; + mIsNotAWord = isNotAWord; + mIsBlacklistEntry = isBlacklistEntry; + mHasBigrams = bigrams != null && !bigrams.isEmpty(); + mHasShortcuts = shortcutTargets != null && !shortcutTargets.isEmpty(); + } + + private static ProbabilityInfo createProbabilityInfoFromArray(final int[] probabilityInfo) { + return new ProbabilityInfo( + probabilityInfo[BinaryDictionary.FORMAT_WORD_PROPERTY_PROBABILITY_INDEX], + probabilityInfo[BinaryDictionary.FORMAT_WORD_PROPERTY_TIMESTAMP_INDEX], + probabilityInfo[BinaryDictionary.FORMAT_WORD_PROPERTY_LEVEL_INDEX], + probabilityInfo[BinaryDictionary.FORMAT_WORD_PROPERTY_COUNT_INDEX]); + } + + // Construct word property using information from native code. + // This represents invalid word when the probability is BinaryDictionary.NOT_A_PROBABILITY. + public WordProperty(final int[] codePoints, final boolean isNotAWord, + final boolean isBlacklisted, final boolean hasBigram, final boolean hasShortcuts, + final boolean isBeginningOfSentence, final int[] probabilityInfo, + final ArrayList<int[]> bigramTargets, final ArrayList<int[]> bigramProbabilityInfo, + final ArrayList<int[]> shortcutTargets, + final ArrayList<Integer> shortcutProbabilities) { + mWord = StringUtils.getStringFromNullTerminatedCodePointArray(codePoints); + mProbabilityInfo = createProbabilityInfoFromArray(probabilityInfo); + mShortcutTargets = new ArrayList<>(); + mBigrams = new ArrayList<>(); + mIsBeginningOfSentence = isBeginningOfSentence; + mIsNotAWord = isNotAWord; + mIsBlacklistEntry = isBlacklisted; + mHasShortcuts = hasShortcuts; + mHasBigrams = hasBigram; + + final int bigramTargetCount = bigramTargets.size(); + for (int i = 0; i < bigramTargetCount; i++) { + final String bigramTargetString = + StringUtils.getStringFromNullTerminatedCodePointArray(bigramTargets.get(i)); + mBigrams.add(new WeightedString(bigramTargetString, + createProbabilityInfoFromArray(bigramProbabilityInfo.get(i)))); + } + + final int shortcutTargetCount = shortcutTargets.size(); + for (int i = 0; i < shortcutTargetCount; i++) { + final String shortcutTargetString = + StringUtils.getStringFromNullTerminatedCodePointArray(shortcutTargets.get(i)); + mShortcutTargets.add( + new WeightedString(shortcutTargetString, shortcutProbabilities.get(i))); + } + } + + public int getProbability() { + return mProbabilityInfo.mProbability; + } + + private static int computeHashCode(WordProperty word) { + return Arrays.hashCode(new Object[] { + word.mWord, + word.mProbabilityInfo, + word.mShortcutTargets.hashCode(), + word.mBigrams.hashCode(), + word.mIsNotAWord, + word.mIsBlacklistEntry + }); + } + + /** + * Three-way comparison. + * + * A Word x is greater than a word y if x has a higher frequency. If they have the same + * frequency, they are sorted in lexicographic order. + */ + @Override + public int compareTo(final WordProperty w) { + if (getProbability() < w.getProbability()) return 1; + if (getProbability() > w.getProbability()) return -1; + return mWord.compareTo(w.mWord); + } + + /** + * Equality test. + * + * Words are equal if they have the same frequency, the same spellings, and the same + * attributes. + */ + @Override + public boolean equals(Object o) { + if (o == this) return true; + if (!(o instanceof WordProperty)) return false; + WordProperty w = (WordProperty)o; + return mProbabilityInfo.equals(w.mProbabilityInfo) && mWord.equals(w.mWord) + && mShortcutTargets.equals(w.mShortcutTargets) && mBigrams.equals(w.mBigrams) + && mIsNotAWord == w.mIsNotAWord && mIsBlacklistEntry == w.mIsBlacklistEntry + && mHasBigrams == w.mHasBigrams && mHasShortcuts && w.mHasBigrams; + } + + @Override + public int hashCode() { + if (mHashCode == 0) { + mHashCode = computeHashCode(this); + } + return mHashCode; + } + + @UsedForTesting + public boolean isValid() { + return getProbability() != BinaryDictionary.NOT_A_PROBABILITY; + } + + @Override + public String toString() { + return CombinedFormatUtils.formatWordProperty(this); + } +} diff --git a/java/src/com/android/inputmethod/latin/personalization/AccountUtils.java b/java/src/com/android/inputmethod/latin/personalization/AccountUtils.java index a446672cb..ab3ef964e 100644 --- a/java/src/com/android/inputmethod/latin/personalization/AccountUtils.java +++ b/java/src/com/android/inputmethod/latin/personalization/AccountUtils.java @@ -35,7 +35,7 @@ public class AccountUtils { } public static List<String> getDeviceAccountsEmailAddresses(final Context context) { - final ArrayList<String> retval = new ArrayList<String>(); + final ArrayList<String> retval = new ArrayList<>(); for (final Account account : getAccounts(context)) { final String name = account.name; if (Patterns.EMAIL_ADDRESS.matcher(name).matches()) { @@ -54,7 +54,7 @@ public class AccountUtils { */ public static List<String> getDeviceAccountsWithDomain( final Context context, final String domain) { - final ArrayList<String> retval = new ArrayList<String>(); + final ArrayList<String> retval = new ArrayList<>(); final String atDomain = "@" + domain.toLowerCase(Locale.ROOT); for (final Account account : getAccounts(context)) { if (account.name.toLowerCase(Locale.ROOT).endsWith(atDomain)) { diff --git a/java/src/com/android/inputmethod/latin/personalization/ContextualDictionary.java b/java/src/com/android/inputmethod/latin/personalization/ContextualDictionary.java new file mode 100644 index 000000000..ac55b9333 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/personalization/ContextualDictionary.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.personalization; + +import android.content.Context; + +import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.Dictionary; +import com.android.inputmethod.latin.ExpandableBinaryDictionary; + +import java.io.File; +import java.util.Locale; + +public class ContextualDictionary extends ExpandableBinaryDictionary { + /* package */ static final String NAME = ContextualDictionary.class.getSimpleName(); + + private ContextualDictionary(final Context context, final Locale locale, + final File dictFile) { + super(context, getDictName(NAME, locale, dictFile), locale, Dictionary.TYPE_CONTEXTUAL, + dictFile); + // Always reset the contents. + clear(); + } + + @UsedForTesting + public static ContextualDictionary getDictionary(final Context context, final Locale locale, + final File dictFile, final String dictNamePrefix) { + return new ContextualDictionary(context, locale, dictFile); + } + + @Override + public boolean isValidWord(final String word) { + // Strings out of this dictionary should not be considered existing words. + return false; + } + + @Override + protected void loadInitialContentsLocked() { + } +} diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegister.java b/java/src/com/android/inputmethod/latin/personalization/ContextualDictionaryUpdater.java index c1833ff14..7dc120e06 100644 --- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegister.java +++ b/java/src/com/android/inputmethod/latin/personalization/ContextualDictionaryUpdater.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 The Android Open Source Project + * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,21 +17,21 @@ package com.android.inputmethod.latin.personalization; import android.content.Context; -import android.content.res.Configuration; -public class PersonalizationDictionarySessionRegister { - public static void init(Context context) { - } +import com.android.inputmethod.latin.DictionaryFacilitator; - public static void onConfigurationChanged(final Context context, final Configuration conf) { +public class ContextualDictionaryUpdater { + public ContextualDictionaryUpdater(final Context context, + final DictionaryFacilitator dictionaryFacilitator, + final Runnable onUpdateRunnable) { } - public static void onUpdateData(Context context, String type) { + public void onLoadSettings(final boolean usePersonalizedDicts) { } - public static void onRemoveData(Context context, String type) { + public void onStartInputView(final String packageName) { } - public static void onDestroy(Context context) { + public void onDestroy() { } } diff --git a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java index 1de15a333..1ba7b366f 100644 --- a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java +++ b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java @@ -17,25 +17,13 @@ package com.android.inputmethod.latin.personalization; import android.content.Context; -import android.content.SharedPreferences; -import android.util.Log; -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.Dictionary; import com.android.inputmethod.latin.ExpandableBinaryDictionary; -import com.android.inputmethod.latin.LatinImeLogger; -import com.android.inputmethod.latin.makedict.DictDecoder; -import com.android.inputmethod.latin.makedict.FormatSpec; -import com.android.inputmethod.latin.settings.Settings; -import com.android.inputmethod.latin.utils.CollectionUtils; -import com.android.inputmethod.latin.utils.UserHistoryDictIOUtils; -import com.android.inputmethod.latin.utils.UserHistoryDictIOUtils.OnAddWordListener; +import com.android.inputmethod.latin.makedict.DictionaryHeader; import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; +import java.util.Locale; import java.util.Map; /** @@ -43,10 +31,7 @@ import java.util.Map; * model. */ public abstract class DecayingExpandableBinaryDictionaryBase extends ExpandableBinaryDictionary { - private static final String TAG = DecayingExpandableBinaryDictionaryBase.class.getSimpleName(); - public static final boolean DBG_SAVE_RESTORE = false; - private static final boolean DBG_STRESS_TEST = false; - private static final boolean PROFILE_SAVE_RESTORE = LatinImeLogger.sDBG; + private static final boolean DBG_DUMP_ON_CLOSE = false; /** Any pair being typed or picked */ public static final int FREQUENCY_FOR_TYPED = 2; @@ -54,182 +39,51 @@ public abstract class DecayingExpandableBinaryDictionaryBase extends ExpandableB public static final int FREQUENCY_FOR_WORDS_IN_DICTS = FREQUENCY_FOR_TYPED; public static final int FREQUENCY_FOR_WORDS_NOT_IN_DICTS = Dictionary.NOT_A_PROBABILITY; - /** Locale for which this user history dictionary is storing words */ - private final String mLocale; + /** The locale for this dictionary. */ + public final Locale mLocale; - private final String mFileName; - - private final SharedPreferences mPrefs; - - private final ArrayList<PersonalizationDictionaryUpdateSession> mSessions = - CollectionUtils.newArrayList(); - - // Should always be false except when we use this class for test - @UsedForTesting boolean mIsTest = false; - - /* package */ DecayingExpandableBinaryDictionaryBase(final Context context, - final String locale, final SharedPreferences sp, final String dictionaryType, - final String fileName) { - super(context, fileName, dictionaryType, true); + protected DecayingExpandableBinaryDictionaryBase(final Context context, + final String dictName, final Locale locale, final String dictionaryType, + final File dictFile) { + super(context, dictName, locale, dictionaryType, dictFile); mLocale = locale; - mFileName = fileName; - mPrefs = sp; - if (mLocale != null && mLocale.length() > 1) { - asyncLoadDictionaryToMemory(); + if (mLocale != null && mLocale.toString().length() > 1) { reloadDictionaryIfRequired(); } } @Override public void close() { - if (!ExpandableBinaryDictionary.ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { - closeBinaryDictionary(); + if (DBG_DUMP_ON_CLOSE) { + dumpAllWordsForDebug(); } // Flush pending writes. - // TODO: Remove after this class become to use a dynamic binary dictionary. - asyncFlashAllBinaryDictionary(); - Settings.writeLastUserHistoryWriteTime(mPrefs, mLocale); + asyncFlushBinaryDictionary(); + super.close(); } @Override protected Map<String, String> getHeaderAttributeMap() { - HashMap<String, String> attributeMap = new HashMap<String, String>(); - attributeMap.put(FormatSpec.FileHeader.SUPPORTS_DYNAMIC_UPDATE_ATTRIBUTE, - FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE); - attributeMap.put(FormatSpec.FileHeader.USES_FORGETTING_CURVE_ATTRIBUTE, - FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE); - attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE, mFileName); - attributeMap.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_ATTRIBUTE, mLocale); + final Map<String, String> attributeMap = super.getHeaderAttributeMap(); + attributeMap.put(DictionaryHeader.USES_FORGETTING_CURVE_KEY, + DictionaryHeader.ATTRIBUTE_VALUE_TRUE); + attributeMap.put(DictionaryHeader.HAS_HISTORICAL_INFO_KEY, + DictionaryHeader.ATTRIBUTE_VALUE_TRUE); return attributeMap; } @Override - protected boolean hasContentChanged() { - return false; - } - - @Override - protected boolean needsToReloadBeforeWriting() { - return false; + protected void loadInitialContentsLocked() { + // No initial contents. } - /** - * Pair will be added to the decaying dictionary. - * - * The first word may be null. That means we don't know the context, in other words, - * it's only a unigram. The first word may also be an empty string : this means start - * context, as in beginning of a sentence for example. - * The second word may not be null (a NullPointerException would be thrown). - */ - public void addToDictionary(final String word0, final String word1, final boolean isValid) { - if (word1.length() >= Constants.DICTIONARY_MAX_WORD_LENGTH || - (word0 != null && word0.length() >= Constants.DICTIONARY_MAX_WORD_LENGTH)) { - return; - } - final int frequency = ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE ? - (isValid ? FREQUENCY_FOR_WORDS_IN_DICTS : FREQUENCY_FOR_WORDS_NOT_IN_DICTS) : - FREQUENCY_FOR_TYPED; - addWordDynamically(word1, null /* shortcutTarget */, frequency, 0 /* shortcutFreq */, - false /* isNotAWord */); - // Do not insert a word as a bigram of itself - if (word1.equals(word0)) { - return; - } - if (null != word0) { - addBigramDynamically(word0, word1, frequency, isValid); - } - } - - public void cancelAddingUserHistory(final String word0, final String word1) { - removeBigramDynamically(word0, word1); + /* package */ void runGCIfRequired() { + runGCIfRequired(false /* mindsBlockByGC */); } @Override - protected void loadDictionaryAsync() { - final int[] profTotalCount = { 0 }; - final String locale = getLocale(); - if (DBG_STRESS_TEST) { - try { - Log.w(TAG, "Start stress in loading: " + locale); - Thread.sleep(15000); - Log.w(TAG, "End stress in loading"); - } catch (InterruptedException e) { - } - } - final long last = Settings.readLastUserHistoryWriteTime(mPrefs, locale); - final long now = System.currentTimeMillis(); - final ExpandableBinaryDictionary dictionary = this; - final OnAddWordListener listener = new OnAddWordListener() { - @Override - public void setUnigram(final String word, final String shortcutTarget, - final int frequency, final int shortcutFreq) { - if (DBG_SAVE_RESTORE) { - Log.d(TAG, "load unigram: " + word + "," + frequency); - } - addWord(word, shortcutTarget, frequency, shortcutFreq, false /* isNotAWord */); - ++profTotalCount[0]; - } - - @Override - public void setBigram(final String word0, final String word1, final int frequency) { - if (word0.length() < Constants.DICTIONARY_MAX_WORD_LENGTH - && word1.length() < Constants.DICTIONARY_MAX_WORD_LENGTH) { - if (DBG_SAVE_RESTORE) { - Log.d(TAG, "load bigram: " + word0 + "," + word1 + "," + frequency); - } - ++profTotalCount[0]; - addBigram(word0, word1, frequency, last); - } - } - }; - - // Load the dictionary from binary file - final File dictFile = new File(mContext.getFilesDir(), mFileName); - final DictDecoder dictDecoder = FormatSpec.getDictDecoder(dictFile, - DictDecoder.USE_BYTEARRAY); - if (dictDecoder == null) { - // This is an expected condition: we don't have a user history dictionary for this - // language yet. It will be created sometime later. - return; - } - - try { - dictDecoder.openDictBuffer(); - UserHistoryDictIOUtils.readDictionaryBinary(dictDecoder, listener); - } catch (IOException e) { - Log.d(TAG, "IOException on opening a bytebuffer", e); - } finally { - if (PROFILE_SAVE_RESTORE) { - final long diff = System.currentTimeMillis() - now; - Log.d(TAG, "PROF: Load UserHistoryDictionary: " - + locale + ", " + diff + "ms. load " + profTotalCount[0] + "entries."); - } - } - } - - protected String getLocale() { - return mLocale; - } - - public void registerUpdateSession(PersonalizationDictionaryUpdateSession session) { - session.setPredictionDictionary(this); - mSessions.add(session); - session.onDictionaryReady(); - } - - public void unRegisterUpdateSession(PersonalizationDictionaryUpdateSession session) { - mSessions.remove(session); - } - - @UsedForTesting - public void clearAndFlushDictionary() { - // Clear the node structure on memory - clear(); - // Then flush the cleared state of the dictionary on disk. - asyncFlashAllBinaryDictionary(); - } - - /* package */ void decayIfNeeded() { - runGCIfRequired(false /* mindsBlockByGC */); + public boolean isValidWord(final String word) { + // Strings out of this dictionary should not be considered existing words. + return false; } } diff --git a/java/src/com/android/inputmethod/latin/personalization/DictionaryDecayBroadcastReciever.java b/java/src/com/android/inputmethod/latin/personalization/DictionaryDecayBroadcastReciever.java index e9ca662e7..221bb9a8f 100644 --- a/java/src/com/android/inputmethod/latin/personalization/DictionaryDecayBroadcastReciever.java +++ b/java/src/com/android/inputmethod/latin/personalization/DictionaryDecayBroadcastReciever.java @@ -43,7 +43,7 @@ public class DictionaryDecayBroadcastReciever extends BroadcastReceiver { /** * Interval to update for decaying dictionaries. */ - private static final long DICTIONARY_DECAY_INTERVAL = TimeUnit.MINUTES.toMillis(60); + /* package */ static final long DICTIONARY_DECAY_INTERVAL = TimeUnit.MINUTES.toMillis(60); public static void setUpIntervalAlarmForDictionaryDecaying(Context context) { AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); @@ -60,7 +60,8 @@ public class DictionaryDecayBroadcastReciever extends BroadcastReceiver { public void onReceive(final Context context, final Intent intent) { final String action = intent.getAction(); if (action.equals(DICTIONARY_DECAY_INTENT_ACTION)) { - PersonalizationHelper.tryDecayingAllOpeningUserHistoryDictionary(); + PersonalizationHelper.runGCOnAllOpenedUserHistoryDictionaries(); + PersonalizationHelper.runGCOnAllOpenedPersonalizationDictionaries(); } } } diff --git a/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java b/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java deleted file mode 100644 index 6f152bb91..000000000 --- a/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.personalization; - -import android.content.Context; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.compat.ActivityManagerCompatUtils; -import com.android.inputmethod.keyboard.ProximityInfo; -import com.android.inputmethod.latin.AbstractDictionaryWriter; -import com.android.inputmethod.latin.ExpandableDictionary; -import com.android.inputmethod.latin.WordComposer; -import com.android.inputmethod.latin.ExpandableDictionary.NextWord; -import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; -import com.android.inputmethod.latin.makedict.DictEncoder; -import com.android.inputmethod.latin.makedict.FormatSpec; -import com.android.inputmethod.latin.makedict.UnsupportedFormatException; -import com.android.inputmethod.latin.utils.UserHistoryDictIOUtils; -import com.android.inputmethod.latin.utils.UserHistoryDictIOUtils.BigramDictionaryInterface; -import com.android.inputmethod.latin.utils.UserHistoryForgettingCurveUtils; -import com.android.inputmethod.latin.utils.UserHistoryForgettingCurveUtils.ForgettingCurveParams; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Map; - -// Currently this class is used to implement dynamic prodiction dictionary. -// TODO: Move to native code. -public class DynamicPersonalizationDictionaryWriter extends AbstractDictionaryWriter { - private static final String TAG = DynamicPersonalizationDictionaryWriter.class.getSimpleName(); - /** Maximum number of pairs. Pruning will start when databases goes above this number. */ - public static final int DEFAULT_MAX_HISTORY_BIGRAMS = 10000; - public static final int LOW_MEMORY_MAX_HISTORY_BIGRAMS = 2000; - - /** Any pair being typed or picked */ - private static final int FREQUENCY_FOR_TYPED = 2; - - private static final int BINARY_DICT_VERSION = 3; - private static final FormatSpec.FormatOptions FORMAT_OPTIONS = - new FormatSpec.FormatOptions(BINARY_DICT_VERSION, true /* supportsDynamicUpdate */); - - private final UserHistoryDictionaryBigramList mBigramList = - new UserHistoryDictionaryBigramList(); - private final ExpandableDictionary mExpandableDictionary; - private final int mMaxHistoryBigrams; - - public DynamicPersonalizationDictionaryWriter(final Context context, final String dictType) { - super(context, dictType); - mExpandableDictionary = new ExpandableDictionary(dictType); - final boolean isLowRamDevice = ActivityManagerCompatUtils.isLowRamDevice(context); - mMaxHistoryBigrams = isLowRamDevice ? - LOW_MEMORY_MAX_HISTORY_BIGRAMS : DEFAULT_MAX_HISTORY_BIGRAMS; - } - - @Override - public void clear() { - mBigramList.evictAll(); - mExpandableDictionary.clearDictionary(); - } - - /** - * Adds a word unigram to the fusion dictionary. Call updateBinaryDictionary when all changes - * are done to update the binary dictionary. - * @param word The word to add. - * @param shortcutTarget A shortcut target for this word, or null if none. - * @param frequency The frequency for this unigram. - * @param shortcutFreq The frequency of the shortcut (0~15, with 15 = whitelist). Ignored - * if shortcutTarget is null. - * @param isNotAWord true if this is not a word, i.e. shortcut only. - */ - @Override - public void addUnigramWord(final String word, final String shortcutTarget, final int frequency, - final int shortcutFreq, final boolean isNotAWord) { - if (mBigramList.size() > mMaxHistoryBigrams * 2) { - // Too many entries: just stop adding new vocabulary and wait next refresh. - return; - } - mExpandableDictionary.addWord(word, shortcutTarget, frequency, shortcutFreq); - mBigramList.addBigram(null, word, (byte)frequency); - } - - @Override - public void addBigramWords(final String word0, final String word1, final int frequency, - final boolean isValid, final long lastModifiedTime) { - if (mBigramList.size() > mMaxHistoryBigrams * 2) { - // Too many entries: just stop adding new vocabulary and wait next refresh. - return; - } - if (lastModifiedTime > 0) { - mExpandableDictionary.setBigramAndGetFrequency(word0, word1, - new ForgettingCurveParams(frequency, System.currentTimeMillis(), - lastModifiedTime)); - mBigramList.addBigram(word0, word1, (byte)frequency); - } else { - mExpandableDictionary.setBigramAndGetFrequency(word0, word1, - new ForgettingCurveParams(isValid)); - mBigramList.addBigram(word0, word1, (byte)frequency); - } - } - - @Override - public void removeBigramWords(final String word0, final String word1) { - if (mBigramList.removeBigram(word0, word1)) { - mExpandableDictionary.removeBigram(word0, word1); - } - } - - @Override - protected void writeDictionary(final DictEncoder dictEncoder, - final Map<String, String> attributeMap) throws IOException, UnsupportedFormatException { - UserHistoryDictIOUtils.writeDictionary(dictEncoder, - new FrequencyProvider(mBigramList, mExpandableDictionary, mMaxHistoryBigrams), - mBigramList, FORMAT_OPTIONS); - } - - private static class FrequencyProvider implements BigramDictionaryInterface { - private final UserHistoryDictionaryBigramList mBigramList; - private final ExpandableDictionary mExpandableDictionary; - private final int mMaxHistoryBigrams; - - public FrequencyProvider(final UserHistoryDictionaryBigramList bigramList, - final ExpandableDictionary expandableDictionary, final int maxHistoryBigrams) { - mBigramList = bigramList; - mExpandableDictionary = expandableDictionary; - mMaxHistoryBigrams = maxHistoryBigrams; - } - - @Override - public int getFrequency(final String word0, final String word1) { - final int freq; - if (word0 == null) { // unigram - freq = FREQUENCY_FOR_TYPED; - } else { // bigram - final NextWord nw = mExpandableDictionary.getBigramWord(word0, word1); - if (nw != null) { - final ForgettingCurveParams forgettingCurveParams = nw.getFcParams(); - final byte prevFc = mBigramList.getBigrams(word0).get(word1); - final byte fc = forgettingCurveParams.getFc(); - final boolean isValid = forgettingCurveParams.isValid(); - if (prevFc > 0 && prevFc == fc) { - freq = fc & 0xFF; - } else if (UserHistoryForgettingCurveUtils. - needsToSave(fc, isValid, mBigramList.size() <= mMaxHistoryBigrams)) { - freq = fc & 0xFF; - } else { - // Delete this entry - freq = -1; - } - } else { - // Delete this entry - freq = -1; - } - } - return freq; - } - } - - @Override - public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, - final String prevWord, final ProximityInfo proximityInfo, - boolean blockOffensiveWords, final int[] additionalFeaturesOptions) { - return mExpandableDictionary.getSuggestions(composer, prevWord, proximityInfo, - blockOffensiveWords, additionalFeaturesOptions); - } - - @Override - public boolean isValidWord(final String word) { - return mExpandableDictionary.isValidWord(word); - } - - @UsedForTesting - public boolean isInBigramListForTests(final String word) { - // TODO: Use native method to determine whether the word is in dictionary or not - return mBigramList.containsKey(word) || mBigramList.getBigrams(null).containsKey(word); - } -} diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDataChunk.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDataChunk.java new file mode 100644 index 000000000..9d72de8c5 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDataChunk.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.personalization; + +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +public class PersonalizationDataChunk { + public final boolean mInputByUser; + public final List<String> mTokens; + public final int mTimestampInSeconds; + public final String mPackageName; + public final Locale mlocale = null; + + public PersonalizationDataChunk(boolean inputByUser, final List<String> tokens, + final int timestampInSeconds, final String packageName) { + mInputByUser = inputByUser; + mTokens = Collections.unmodifiableList(tokens); + mTimestampInSeconds = timestampInSeconds; + mPackageName = packageName; + } +} diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java index f257165cb..f2ad22ac7 100644 --- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java +++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java @@ -16,58 +16,26 @@ package com.android.inputmethod.latin.personalization; -import com.android.inputmethod.latin.Dictionary; -import com.android.inputmethod.latin.ExpandableBinaryDictionary; -import com.android.inputmethod.latin.utils.CollectionUtils; - import android.content.Context; -import android.content.SharedPreferences; - -import java.util.ArrayList; - -/** - * This class is a dictionary for the personalized language model that uses binary dictionary. - */ -public class PersonalizationDictionary extends ExpandableBinaryDictionary { - private static final String NAME = "personalization"; - private final ArrayList<PersonalizationDictionaryUpdateSession> mSessions = - CollectionUtils.newArrayList(); - - /** Locale for which this user history dictionary is storing words */ - private final String mLocale; - public PersonalizationDictionary(final Context context, final String locale, - final SharedPreferences prefs) { - // TODO: Make isUpdatable true. - super(context, getFilenameWithLocale(NAME, locale), Dictionary.TYPE_PERSONALIZATION, - false /* isUpdatable */); - mLocale = locale; - // TODO: Restore last updated time - loadDictionary(); - } - - @Override - protected void loadDictionaryAsync() { - // TODO: Implement - } +import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.Dictionary; - @Override - protected boolean hasContentChanged() { - return false; - } +import java.io.File; +import java.util.Locale; - @Override - protected boolean needsToReloadBeforeWriting() { - return false; - } +public class PersonalizationDictionary extends DecayingExpandableBinaryDictionaryBase { + /* package */ static final String NAME = PersonalizationDictionary.class.getSimpleName(); - public void registerUpdateSession(PersonalizationDictionaryUpdateSession session) { - session.setDictionary(this); - mSessions.add(session); - session.onDictionaryReady(); + // TODO: Make this constructor private + /* package */ PersonalizationDictionary(final Context context, final Locale locale) { + super(context, getDictName(NAME, locale, null /* dictFile */), locale, + Dictionary.TYPE_PERSONALIZATION, null /* dictFile */); } - public void unRegisterUpdateSession(PersonalizationDictionaryUpdateSession session) { - mSessions.remove(session); + @UsedForTesting + public static PersonalizationDictionary getDictionary(final Context context, + final Locale locale, final File dictFile, final String dictNamePrefix) { + return PersonalizationHelper.getPersonalizationDictionary(context, locale); } } diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java deleted file mode 100644 index a86f6e584..000000000 --- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.personalization; - -import android.content.Context; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; - -/** - * This class is a session where a data provider can communicate with a personalization - * dictionary. - */ -public abstract class PersonalizationDictionaryUpdateSession { - /** - * This class is a parameter for a new unigram or bigram word which will be added - * to the personalization dictionary. - */ - public static class PersonalizationLanguageModelParam { - public final String mWord0; - public final String mWord1; - public final boolean mIsValid; - public final int mFrequency; - public PersonalizationLanguageModelParam(String word0, String word1, boolean isValid, - int frequency) { - mWord0 = word0; - mWord1 = word1; - mIsValid = isValid; - mFrequency = frequency; - } - } - - // TODO: Use a dynamic binary dictionary instead - public WeakReference<PersonalizationDictionary> mDictionary; - public WeakReference<DecayingExpandableBinaryDictionaryBase> mPredictionDictionary; - public final String mSystemLocale; - public PersonalizationDictionaryUpdateSession(String locale) { - mSystemLocale = locale; - } - - public abstract void onDictionaryReady(); - - public abstract void onDictionaryClosed(Context context); - - public void setDictionary(PersonalizationDictionary dictionary) { - mDictionary = new WeakReference<PersonalizationDictionary>(dictionary); - } - - public void setPredictionDictionary(DecayingExpandableBinaryDictionaryBase dictionary) { - mPredictionDictionary = - new WeakReference<DecayingExpandableBinaryDictionaryBase>(dictionary); - } - - protected PersonalizationDictionary getDictionary() { - return mDictionary == null ? null : mDictionary.get(); - } - - protected DecayingExpandableBinaryDictionaryBase getPredictionDictionary() { - return mPredictionDictionary == null ? null : mPredictionDictionary.get(); - } - - private void unsetDictionary() { - final PersonalizationDictionary dictionary = getDictionary(); - if (dictionary == null) { - return; - } - dictionary.unRegisterUpdateSession(this); - } - - private void unsetPredictionDictionary() { - final DecayingExpandableBinaryDictionaryBase dictionary = getPredictionDictionary(); - if (dictionary == null) { - return; - } - dictionary.unRegisterUpdateSession(this); - } - - public void clearAndFlushPredictionDictionary(Context context) { - final DecayingExpandableBinaryDictionaryBase dictionary = getPredictionDictionary(); - if (dictionary == null) { - return; - } - dictionary.clearAndFlushDictionary(); - } - - public void closeSession(Context context) { - unsetDictionary(); - unsetPredictionDictionary(); - onDictionaryClosed(context); - } - - // TODO: Support multi locale to add bigram - public void addBigramToPersonalizationDictionary(String word0, String word1, boolean isValid, - int frequency) { - final DecayingExpandableBinaryDictionaryBase dictionary = getPredictionDictionary(); - if (dictionary == null) { - return; - } - dictionary.addToDictionary(word0, word1, isValid); - } - - // Bulk import - // TODO: Support multi locale to add bigram - public void addBigramsToPersonalizationDictionary( - final ArrayList<PersonalizationLanguageModelParam> lmParams) { - final DecayingExpandableBinaryDictionaryBase dictionary = getPredictionDictionary(); - if (dictionary == null) { - return; - } - for (final PersonalizationLanguageModelParam lmParam : lmParams) { - dictionary.addToDictionary(lmParam.mWord0, lmParam.mWord1, lmParam.mIsValid); - } - } -} diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdater.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdater.java new file mode 100644 index 000000000..c97a0d232 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdater.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.personalization; + +import java.util.Locale; + +import android.content.Context; + +import com.android.inputmethod.latin.DictionaryFacilitator; + +public class PersonalizationDictionaryUpdater { + final Context mContext; + final DictionaryFacilitator mDictionaryFacilitator; + boolean mDictCleared = false; + + public PersonalizationDictionaryUpdater(final Context context, + final DictionaryFacilitator dictionaryFacilitator) { + mContext = context; + mDictionaryFacilitator = dictionaryFacilitator; + } + + public Locale getLocale() { + return null; + } + + public void onLoadSettings(final boolean usePersonalizedDicts, + final boolean isSystemLocaleSameAsLocaleOfAllEnabledSubtypesOfEnabledImes) { + if (!mDictCleared) { + // Clear and never update the personalization dictionary. + PersonalizationHelper.removeAllPersonalizationDictionaries(mContext); + mDictionaryFacilitator.clearPersonalizationDictionary(); + mDictCleared = true; + } + } + + public void onDestroy() { + } +} diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java index 221ddeeba..aac40940b 100644 --- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java +++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java @@ -16,36 +16,33 @@ package com.android.inputmethod.latin.personalization; -import com.android.inputmethod.latin.utils.CollectionUtils; - import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; import android.util.Log; +import com.android.inputmethod.latin.utils.FileUtils; + +import java.io.File; +import java.io.FilenameFilter; import java.lang.ref.SoftReference; +import java.util.Locale; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; public class PersonalizationHelper { private static final String TAG = PersonalizationHelper.class.getSimpleName(); private static final boolean DEBUG = false; private static final ConcurrentHashMap<String, SoftReference<UserHistoryDictionary>> - sLangUserHistoryDictCache = CollectionUtils.newConcurrentHashMap(); - + sLangUserHistoryDictCache = new ConcurrentHashMap<>(); private static final ConcurrentHashMap<String, SoftReference<PersonalizationDictionary>> - sLangPersonalizationDictCache = CollectionUtils.newConcurrentHashMap(); - - private static final ConcurrentHashMap<String, - SoftReference<PersonalizationPredictionDictionary>> - sLangPersonalizationPredictionDictCache = - CollectionUtils.newConcurrentHashMap(); + sLangPersonalizationDictCache = new ConcurrentHashMap<>(); public static UserHistoryDictionary getUserHistoryDictionary( - final Context context, final String locale, final SharedPreferences sp) { + final Context context, final Locale locale) { + final String localeStr = locale.toString(); synchronized (sLangUserHistoryDictCache) { - if (sLangUserHistoryDictCache.containsKey(locale)) { + if (sLangUserHistoryDictCache.containsKey(localeStr)) { final SoftReference<UserHistoryDictionary> ref = - sLangUserHistoryDictCache.get(locale); + sLangUserHistoryDictCache.get(localeStr); final UserHistoryDictionary dict = ref == null ? null : ref.get(); if (dict != null) { if (DEBUG) { @@ -55,77 +52,110 @@ public class PersonalizationHelper { return dict; } } - final UserHistoryDictionary dict = new UserHistoryDictionary(context, locale, sp); - sLangUserHistoryDictCache.put(locale, new SoftReference<UserHistoryDictionary>(dict)); + final UserHistoryDictionary dict = new UserHistoryDictionary(context, locale); + sLangUserHistoryDictCache.put(localeStr, new SoftReference<>(dict)); return dict; } } - public static void tryDecayingAllOpeningUserHistoryDictionary() { - for (final ConcurrentHashMap.Entry<String, SoftReference<UserHistoryDictionary>> entry - : sLangUserHistoryDictCache.entrySet()) { - if (entry.getValue() != null) { - final UserHistoryDictionary dict = entry.getValue().get(); - if (dict != null) { - dict.decayIfNeeded(); - } - } + private static int sCurrentTimestampForTesting = 0; + public static void currentTimeChangedForTesting(final int currentTimestamp) { + if (TimeUnit.MILLISECONDS.toSeconds( + DictionaryDecayBroadcastReciever.DICTIONARY_DECAY_INTERVAL) + < currentTimestamp - sCurrentTimestampForTesting) { + runGCOnAllOpenedUserHistoryDictionaries(); + runGCOnAllOpenedPersonalizationDictionaries(); } } - public static void registerPersonalizationDictionaryUpdateSession(final Context context, - final PersonalizationDictionaryUpdateSession session, String locale) { - final PersonalizationPredictionDictionary predictionDictionary = - getPersonalizationPredictionDictionary(context, locale, - PreferenceManager.getDefaultSharedPreferences(context)); - predictionDictionary.registerUpdateSession(session); - final PersonalizationDictionary dictionary = - getPersonalizationDictionary(context, locale, - PreferenceManager.getDefaultSharedPreferences(context)); - dictionary.registerUpdateSession(session); + public static void runGCOnAllOpenedUserHistoryDictionaries() { + runGCOnAllDictionariesIfRequired(sLangUserHistoryDictCache); + } + + public static void runGCOnAllOpenedPersonalizationDictionaries() { + runGCOnAllDictionariesIfRequired(sLangPersonalizationDictCache); + } + + private static <T extends DecayingExpandableBinaryDictionaryBase> + void runGCOnAllDictionariesIfRequired( + final ConcurrentHashMap<String, SoftReference<T>> dictionaryMap) { + for (final ConcurrentHashMap.Entry<String, SoftReference<T>> entry + : dictionaryMap.entrySet()) { + final DecayingExpandableBinaryDictionaryBase dict = entry.getValue().get(); + if (dict != null) { + dict.runGCIfRequired(); + } else { + dictionaryMap.remove(entry.getKey()); + } + } } public static PersonalizationDictionary getPersonalizationDictionary( - final Context context, final String locale, final SharedPreferences sp) { + final Context context, final Locale locale) { + final String localeStr = locale.toString(); synchronized (sLangPersonalizationDictCache) { - if (sLangPersonalizationDictCache.containsKey(locale)) { + if (sLangPersonalizationDictCache.containsKey(localeStr)) { final SoftReference<PersonalizationDictionary> ref = - sLangPersonalizationDictCache.get(locale); + sLangPersonalizationDictCache.get(localeStr); final PersonalizationDictionary dict = ref == null ? null : ref.get(); if (dict != null) { if (DEBUG) { - Log.w(TAG, "Use cached PersonalizationDictCache for " + locale); + Log.w(TAG, "Use cached PersonalizationDictionary for " + locale); } return dict; } } - final PersonalizationDictionary dict = - new PersonalizationDictionary(context, locale, sp); - sLangPersonalizationDictCache.put( - locale, new SoftReference<PersonalizationDictionary>(dict)); + final PersonalizationDictionary dict = new PersonalizationDictionary(context, locale); + sLangPersonalizationDictCache.put(localeStr, new SoftReference<>(dict)); return dict; } } - public static PersonalizationPredictionDictionary getPersonalizationPredictionDictionary( - final Context context, final String locale, final SharedPreferences sp) { - synchronized (sLangPersonalizationPredictionDictCache) { - if (sLangPersonalizationPredictionDictCache.containsKey(locale)) { - final SoftReference<PersonalizationPredictionDictionary> ref = - sLangPersonalizationPredictionDictCache.get(locale); - final PersonalizationPredictionDictionary dict = ref == null ? null : ref.get(); - if (dict != null) { - if (DEBUG) { - Log.w(TAG, "Use cached PersonalizationPredictionDictionary for " + locale); + public static void removeAllPersonalizationDictionaries(final Context context) { + removeAllDictionaries(context, sLangPersonalizationDictCache, + PersonalizationDictionary.NAME); + } + + public static void removeAllUserHistoryDictionaries(final Context context) { + removeAllDictionaries(context, sLangUserHistoryDictCache, + UserHistoryDictionary.NAME); + } + + private static <T extends DecayingExpandableBinaryDictionaryBase> void removeAllDictionaries( + final Context context, final ConcurrentHashMap<String, SoftReference<T>> dictionaryMap, + final String dictNamePrefix) { + synchronized (dictionaryMap) { + for (final ConcurrentHashMap.Entry<String, SoftReference<T>> entry + : dictionaryMap.entrySet()) { + if (entry.getValue() != null) { + final DecayingExpandableBinaryDictionaryBase dict = entry.getValue().get(); + if (dict != null) { + dict.clear(); } - return dict; } } - final PersonalizationPredictionDictionary dict = - new PersonalizationPredictionDictionary(context, locale, sp); - sLangPersonalizationPredictionDictCache.put( - locale, new SoftReference<PersonalizationPredictionDictionary>(dict)); - return dict; + dictionaryMap.clear(); + final File filesDir = context.getFilesDir(); + if (filesDir == null) { + Log.e(TAG, "context.getFilesDir() returned null."); + } + if (!FileUtils.deleteFilteredFiles(filesDir, new DictFilter(dictNamePrefix))) { + Log.e(TAG, "Cannot remove all existing dictionary files. filesDir: " + + filesDir.getAbsolutePath() + ", dictNamePrefix: " + dictNamePrefix); + } + } + } + + private static class DictFilter implements FilenameFilter { + private final String mName; + + DictFilter(final String name) { + mName = name; + } + + @Override + public boolean accept(final File dir, final String name) { + return name.startsWith(mName); } } } diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java deleted file mode 100644 index 432954453..000000000 --- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.personalization; - -import com.android.inputmethod.latin.Dictionary; -import com.android.inputmethod.latin.ExpandableBinaryDictionary; - -import android.content.Context; -import android.content.SharedPreferences; - -public class PersonalizationPredictionDictionary extends DecayingExpandableBinaryDictionaryBase { - private static final String NAME = PersonalizationPredictionDictionary.class.getSimpleName(); - - /* package */ PersonalizationPredictionDictionary(final Context context, final String locale, - final SharedPreferences sp) { - super(context, locale, sp, Dictionary.TYPE_PERSONALIZATION_PREDICTION_IN_JAVA, - getDictionaryFileName(locale)); - } - - private static String getDictionaryFileName(final String locale) { - return NAME + "." + locale + ExpandableBinaryDictionary.DICT_FILE_EXTENSION; - } -} diff --git a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java index a60226d7e..a98b0f156 100644 --- a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java +++ b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java @@ -16,25 +16,72 @@ package com.android.inputmethod.latin.personalization; +import android.content.Context; + +import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.Dictionary; import com.android.inputmethod.latin.ExpandableBinaryDictionary; +import com.android.inputmethod.latin.PrevWordsInfo; +import com.android.inputmethod.latin.utils.DistracterFilter; -import android.content.Context; -import android.content.SharedPreferences; +import java.io.File; +import java.util.Locale; /** * Locally gathers stats about the words user types and various other signals like auto-correction * cancellation or manual picks. This allows the keyboard to adapt to the typist over time. */ public class UserHistoryDictionary extends DecayingExpandableBinaryDictionaryBase { - /* package for tests */ static final String NAME = - UserHistoryDictionary.class.getSimpleName(); - /* package */ UserHistoryDictionary(final Context context, final String locale, - final SharedPreferences sp) { - super(context, locale, sp, Dictionary.TYPE_USER_HISTORY, getDictionaryFileName(locale)); + /* package */ static final String NAME = UserHistoryDictionary.class.getSimpleName(); + + // TODO: Make this constructor private + /* package */ UserHistoryDictionary(final Context context, final Locale locale) { + super(context, getDictName(NAME, locale, null /* dictFile */), locale, + Dictionary.TYPE_USER_HISTORY, null /* dictFile */); + } + + @UsedForTesting + public static UserHistoryDictionary getDictionary(final Context context, final Locale locale, + final File dictFile, final String dictNamePrefix) { + return PersonalizationHelper.getUserHistoryDictionary(context, locale); } - private static String getDictionaryFileName(final String locale) { - return NAME + "." + locale + ExpandableBinaryDictionary.DICT_FILE_EXTENSION; + /** + * Add a word to the user history dictionary. + * + * @param userHistoryDictionary the user history dictionary + * @param prevWordsInfo the information of previous words + * @param word the word the user inputted + * @param isValid whether the word is valid or not + * @param timestamp the timestamp when the word has been inputted + * @param distracterFilter the filter to check whether the word is a distracter + */ + public static void addToDictionary(final ExpandableBinaryDictionary userHistoryDictionary, + final PrevWordsInfo prevWordsInfo, final String word, final boolean isValid, + final int timestamp, final DistracterFilter distracterFilter) { + final String prevWord = prevWordsInfo.mPrevWordsInfo[0].mWord; + if (word.length() >= Constants.DICTIONARY_MAX_WORD_LENGTH || + (prevWord != null && prevWord.length() >= Constants.DICTIONARY_MAX_WORD_LENGTH)) { + return; + } + final int frequency = isValid ? + FREQUENCY_FOR_WORDS_IN_DICTS : FREQUENCY_FOR_WORDS_NOT_IN_DICTS; + userHistoryDictionary.addUnigramEntryWithCheckingDistracter(word, frequency, + null /* shortcutTarget */, 0 /* shortcutFreq */, false /* isNotAWord */, + false /* isBlacklisted */, timestamp, distracterFilter); + // Do not insert a word as a bigram of itself + if (word.equals(prevWord)) { + return; + } + if (null != prevWord) { + if (prevWordsInfo.mPrevWordsInfo[0].mIsBeginningOfSentence) { + // Beginning-of-Sentence n-gram entry is treated as a n-gram entry of invalid word. + userHistoryDictionary.addNgramEntry(prevWordsInfo, word, + FREQUENCY_FOR_WORDS_NOT_IN_DICTS, timestamp); + } else { + userHistoryDictionary.addNgramEntry(prevWordsInfo, word, frequency, timestamp); + } + } } } diff --git a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryBigramList.java b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryBigramList.java deleted file mode 100644 index 55a90ee51..000000000 --- a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryBigramList.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.personalization; - -import android.util.Log; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.utils.CollectionUtils; - -import java.util.HashMap; -import java.util.Set; - -/** - * A store of bigrams which will be updated when the user history dictionary is closed - * All bigrams including stale ones in SQL DB should be stored in this class to avoid adding stale - * bigrams when we write to the SQL DB. - */ -@UsedForTesting -public final class UserHistoryDictionaryBigramList { - public static final byte FORGETTING_CURVE_INITIAL_VALUE = 0; - private static final String TAG = UserHistoryDictionaryBigramList.class.getSimpleName(); - private static final HashMap<String, Byte> EMPTY_BIGRAM_MAP = CollectionUtils.newHashMap(); - private final HashMap<String, HashMap<String, Byte>> mBigramMap = CollectionUtils.newHashMap(); - private int mSize = 0; - - public void evictAll() { - mSize = 0; - mBigramMap.clear(); - } - - /** - * Called when the user typed a word. - */ - @UsedForTesting - public void addBigram(String word1, String word2) { - addBigram(word1, word2, FORGETTING_CURVE_INITIAL_VALUE); - } - - /** - * Called when loaded from the SQL DB. - */ - public void addBigram(String word1, String word2, byte fcValue) { - if (DecayingExpandableBinaryDictionaryBase.DBG_SAVE_RESTORE) { - Log.d(TAG, "--- add bigram: " + word1 + ", " + word2 + ", " + fcValue); - } - final HashMap<String, Byte> map; - if (mBigramMap.containsKey(word1)) { - map = mBigramMap.get(word1); - } else { - map = CollectionUtils.newHashMap(); - mBigramMap.put(word1, map); - } - if (!map.containsKey(word2)) { - ++mSize; - map.put(word2, fcValue); - } - } - - /** - * Called when inserted to the SQL DB. - */ - public void updateBigram(String word1, String word2, byte fcValue) { - if (DecayingExpandableBinaryDictionaryBase.DBG_SAVE_RESTORE) { - Log.d(TAG, "--- update bigram: " + word1 + ", " + word2 + ", " + fcValue); - } - final HashMap<String, Byte> map; - if (mBigramMap.containsKey(word1)) { - map = mBigramMap.get(word1); - } else { - return; - } - if (!map.containsKey(word2)) { - return; - } - map.put(word2, fcValue); - } - - public int size() { - return mSize; - } - - public boolean isEmpty() { - return mBigramMap.isEmpty(); - } - - public boolean containsKey(String word) { - return mBigramMap.containsKey(word); - } - - public Set<String> keySet() { - return mBigramMap.keySet(); - } - - public HashMap<String, Byte> getBigrams(String word1) { - if (mBigramMap.containsKey(word1)) return mBigramMap.get(word1); - // TODO: lower case according to locale - final String lowerWord1 = word1.toLowerCase(); - if (mBigramMap.containsKey(lowerWord1)) return mBigramMap.get(lowerWord1); - return EMPTY_BIGRAM_MAP; - } - - public boolean removeBigram(String word1, String word2) { - final HashMap<String, Byte> set = getBigrams(word1); - if (set.isEmpty()) { - return false; - } - if (set.containsKey(word2)) { - set.remove(word2); - --mSize; - return true; - } - return false; - } -} diff --git a/java/src/com/android/inputmethod/latin/settings/AdditionalSubtypeSettings.java b/java/src/com/android/inputmethod/latin/settings/AdditionalSubtypeSettings.java index 4bf524cbb..ad411f9ee 100644 --- a/java/src/com/android/inputmethod/latin/settings/AdditionalSubtypeSettings.java +++ b/java/src/com/android/inputmethod/latin/settings/AdditionalSubtypeSettings.java @@ -16,8 +16,6 @@ package com.android.inputmethod.latin.settings; -import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.ASCII_CAPABLE; - import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; @@ -44,10 +42,12 @@ import android.widget.Spinner; import android.widget.SpinnerAdapter; import android.widget.Toast; +import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils; +import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.RichInputMethodManager; import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils; -import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.latin.utils.DialogUtils; import com.android.inputmethod.latin.utils.IntentUtils; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; @@ -100,7 +100,7 @@ public final class AdditionalSubtypeSettings extends PreferenceFragment { super(context, android.R.layout.simple_spinner_item); setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - final TreeSet<SubtypeLocaleItem> items = CollectionUtils.newTreeSet(); + final TreeSet<SubtypeLocaleItem> items = new TreeSet<>(); final InputMethodInfo imi = RichInputMethodManager.getInstance() .getInputMethodInfoOfThisIme(); final int count = imi.getSubtypeCount(); @@ -111,7 +111,7 @@ public final class AdditionalSubtypeSettings extends PreferenceFragment { subtype.getLocale(), subtype.hashCode(), subtype.hashCode(), SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype))); } - if (subtype.containsExtraValueKey(ASCII_CAPABLE)) { + if (InputMethodSubtypeCompatUtils.isAsciiCapable(subtype)) { items.add(createItem(context, subtype.getLocale())); } } @@ -150,8 +150,9 @@ public final class AdditionalSubtypeSettings extends PreferenceFragment { // TODO: Should filter out already existing combinations of locale and layout. for (final String layout : SubtypeLocaleUtils.getPredefinedKeyboardLayoutSet()) { // This is a dummy subtype with NO_LANGUAGE, only for display. - final InputMethodSubtype subtype = AdditionalSubtypeUtils.createAdditionalSubtype( - SubtypeLocaleUtils.NO_LANGUAGE, layout, null); + final InputMethodSubtype subtype = + AdditionalSubtypeUtils.createDummyAdditionalSubtype( + SubtypeLocaleUtils.NO_LANGUAGE, layout); add(new KeyboardLayoutSetItem(subtype)); } } @@ -286,8 +287,9 @@ public final class AdditionalSubtypeSettings extends PreferenceFragment { (SubtypeLocaleItem) mSubtypeLocaleSpinner.getSelectedItem(); final KeyboardLayoutSetItem layout = (KeyboardLayoutSetItem) mKeyboardLayoutSetSpinner.getSelectedItem(); - final InputMethodSubtype subtype = AdditionalSubtypeUtils.createAdditionalSubtype( - locale.first, layout.first, ASCII_CAPABLE); + final InputMethodSubtype subtype = + AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype( + locale.first, layout.first); setSubtype(subtype); notifyChanged(); if (isEditing) { @@ -368,7 +370,6 @@ public final class AdditionalSubtypeSettings extends PreferenceFragment { mSubtype = (InputMethodSubtype)source.readParcelable(null); } - @SuppressWarnings("hiding") public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() { @Override @@ -515,9 +516,9 @@ public final class AdditionalSubtypeSettings extends PreferenceFragment { localeString, keyboardLayoutSetName); } - private AlertDialog createDialog( - @SuppressWarnings("unused") final SubtypePreference subtypePref) { - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + private AlertDialog createDialog(final SubtypePreference subtypePref) { + final AlertDialog.Builder builder = new AlertDialog.Builder( + DialogUtils.getPlatformDialogThemeContext(getActivity())); builder.setTitle(R.string.custom_input_styles_title) .setMessage(R.string.custom_input_style_note_message) .setNegativeButton(R.string.not_now, null) @@ -553,7 +554,7 @@ public final class AdditionalSubtypeSettings extends PreferenceFragment { private InputMethodSubtype[] getSubtypes() { final PreferenceGroup group = getPreferenceScreen(); - final ArrayList<InputMethodSubtype> subtypes = CollectionUtils.newArrayList(); + final ArrayList<InputMethodSubtype> subtypes = new ArrayList<>(); final int count = group.getPreferenceCount(); for (int i = 0; i < count; i++) { final Preference pref = group.getPreference(i); diff --git a/java/src/com/android/inputmethod/latin/settings/DebugSettings.java b/java/src/com/android/inputmethod/latin/settings/DebugSettings.java index da1fb73fe..c17e86892 100644 --- a/java/src/com/android/inputmethod/latin/settings/DebugSettings.java +++ b/java/src/com/android/inputmethod/latin/settings/DebugSettings.java @@ -16,37 +16,47 @@ package com.android.inputmethod.latin.settings; +import android.content.Intent; import android.content.SharedPreferences; +import android.content.res.Resources; import android.os.Bundle; import android.os.Process; -import android.preference.CheckBoxPreference; import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceFragment; +import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; +import android.preference.TwoStatePreference; -import com.android.inputmethod.keyboard.KeyboardSwitcher; -import com.android.inputmethod.latin.LatinImeLogger; +import com.android.inputmethod.latin.DictionaryDumpBroadcastReceiver; +import com.android.inputmethod.latin.DictionaryFacilitator; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.debug.ExternalDictionaryGetterForDebug; import com.android.inputmethod.latin.utils.ApplicationUtils; +import com.android.inputmethod.latin.utils.ResourceUtils; public final class DebugSettings extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener { public static final String PREF_DEBUG_MODE = "debug_mode"; public static final String PREF_FORCE_NON_DISTINCT_MULTITOUCH = "force_non_distinct_multitouch"; - public static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode"; - public static final String PREF_STATISTICS_LOGGING = "enable_logging"; - public static final String PREF_USE_ONLY_PERSONALIZATION_DICTIONARY_FOR_DEBUG = - "use_only_personalization_dictionary_for_debug"; - public static final String PREF_BOOST_PERSONALIZATION_DICTIONARY_FOR_DEBUG = - "boost_personalization_dictionary_for_debug"; + public static final String PREF_KEY_PREVIEW_SHOW_UP_START_SCALE = + "pref_key_preview_show_up_start_scale"; + public static final String PREF_KEY_PREVIEW_DISMISS_END_SCALE = + "pref_key_preview_dismiss_end_scale"; + public static final String PREF_KEY_PREVIEW_SHOW_UP_DURATION = + "pref_key_preview_show_up_duration"; + public static final String PREF_KEY_PREVIEW_DISMISS_DURATION = + "pref_key_preview_dismiss_duration"; private static final String PREF_READ_EXTERNAL_DICTIONARY = "read_external_dictionary"; - private static final boolean SHOW_STATISTICS_LOGGING = false; + private static final String PREF_KEY_DUMP_DICTS = "pref_key_dump_dictionaries"; + private static final String PREF_KEY_DUMP_DICT_PREFIX = "pref_key_dump_dictionaries"; + private static final String DICT_NAME_KEY_FOR_EXTRAS = "dict_name"; + public static final String PREF_SLIDING_KEY_INPUT_PREVIEW = "pref_sliding_key_input_preview"; + public static final String PREF_KEY_LONGPRESS_TIMEOUT = "pref_key_longpress_timeout"; private boolean mServiceNeedsRestart = false; - private CheckBoxPreference mDebugMode; - private CheckBoxPreference mStatisticsLoggingPref; + private TwoStatePreference mDebugMode; @Override public void onCreate(Bundle icicle) { @@ -55,21 +65,6 @@ public final 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, - LatinImeLogger.getUsabilityStudyMode(prefs))); - checkbox.setSummary(R.string.settings_warning_researcher_mode); - } - final Preference statisticsLoggingPref = findPreference(PREF_STATISTICS_LOGGING); - if (statisticsLoggingPref instanceof CheckBoxPreference) { - mStatisticsLoggingPref = (CheckBoxPreference) statisticsLoggingPref; - if (!SHOW_STATISTICS_LOGGING) { - getPreferenceScreen().removePreference(statisticsLoggingPref); - } - } - final PreferenceScreen readExternalDictionary = (PreferenceScreen) findPreference(PREF_READ_EXTERNAL_DICTIONARY); if (null != readExternalDictionary) { @@ -85,36 +80,75 @@ public final class DebugSettings extends PreferenceFragment }); } + final PreferenceGroup dictDumpPreferenceGroup = + (PreferenceGroup)findPreference(PREF_KEY_DUMP_DICTS); + final OnPreferenceClickListener dictDumpPrefClickListener = + new DictDumpPrefClickListener(this); + for (final String dictName : DictionaryFacilitator.DICT_TYPE_TO_CLASS.keySet()) { + final Preference preference = new Preference(getActivity()); + preference.setKey(PREF_KEY_DUMP_DICT_PREFIX + dictName); + preference.setTitle("Dump " + dictName + " dictionary"); + preference.setOnPreferenceClickListener(dictDumpPrefClickListener); + preference.getExtras().putString(DICT_NAME_KEY_FOR_EXTRAS, dictName); + dictDumpPreferenceGroup.addPreference(preference); + } + final Resources res = getResources(); + setupKeyLongpressTimeoutSettings(prefs, res); + setupKeyPreviewAnimationDuration(prefs, res, PREF_KEY_PREVIEW_SHOW_UP_DURATION, + res.getInteger(R.integer.config_key_preview_show_up_duration)); + setupKeyPreviewAnimationDuration(prefs, res, PREF_KEY_PREVIEW_DISMISS_DURATION, + res.getInteger(R.integer.config_key_preview_dismiss_duration)); + setupKeyPreviewAnimationScale(prefs, res, PREF_KEY_PREVIEW_SHOW_UP_START_SCALE, + ResourceUtils.getFloatFromFraction( + res, R.fraction.config_key_preview_show_up_start_scale)); + setupKeyPreviewAnimationScale(prefs, res, PREF_KEY_PREVIEW_DISMISS_END_SCALE, + ResourceUtils.getFloatFromFraction( + res, R.fraction.config_key_preview_dismiss_end_scale)); + mServiceNeedsRestart = false; - mDebugMode = (CheckBoxPreference) findPreference(PREF_DEBUG_MODE); + mDebugMode = (TwoStatePreference) findPreference(PREF_DEBUG_MODE); updateDebugMode(); } + private static class DictDumpPrefClickListener implements OnPreferenceClickListener { + final PreferenceFragment mPreferenceFragment; + + public DictDumpPrefClickListener(final PreferenceFragment preferenceFragment) { + mPreferenceFragment = preferenceFragment; + } + + @Override + public boolean onPreferenceClick(final Preference arg0) { + final String dictName = arg0.getExtras().getString(DICT_NAME_KEY_FOR_EXTRAS); + if (dictName != null) { + final Intent intent = + new Intent(DictionaryDumpBroadcastReceiver.DICTIONARY_DUMP_INTENT_ACTION); + intent.putExtra(DictionaryDumpBroadcastReceiver.DICTIONARY_NAME_KEY, dictName); + mPreferenceFragment.getActivity().sendBroadcast(intent); + } + return true; + } + } + @Override public void onStop() { super.onStop(); - if (mServiceNeedsRestart) Process.killProcess(Process.myPid()); + if (mServiceNeedsRestart) { + Process.killProcess(Process.myPid()); + } } @Override public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { - if (key.equals(PREF_DEBUG_MODE)) { - if (mDebugMode != null) { - mDebugMode.setChecked(prefs.getBoolean(PREF_DEBUG_MODE, false)); - final boolean checked = mDebugMode.isChecked(); - if (mStatisticsLoggingPref != null) { - if (checked) { - getPreferenceScreen().addPreference(mStatisticsLoggingPref); - } else { - getPreferenceScreen().removePreference(mStatisticsLoggingPref); - } - } - updateDebugMode(); - mServiceNeedsRestart = true; - } - } else if (key.equals(PREF_FORCE_NON_DISTINCT_MULTITOUCH) - || key.equals(PREF_USE_ONLY_PERSONALIZATION_DICTIONARY_FOR_DEBUG)) { + if (key.equals(PREF_DEBUG_MODE) && mDebugMode != null) { + mDebugMode.setChecked(prefs.getBoolean(PREF_DEBUG_MODE, false)); + updateDebugMode(); + mServiceNeedsRestart = true; + return; + } + if (key.equals(PREF_FORCE_NON_DISTINCT_MULTITOUCH)) { mServiceNeedsRestart = true; + return; } } @@ -133,4 +167,130 @@ public final class DebugSettings extends PreferenceFragment mDebugMode.setSummary(version); } } + + private void setupKeyLongpressTimeoutSettings(final SharedPreferences sp, + final Resources res) { + final SeekBarDialogPreference pref = (SeekBarDialogPreference)findPreference( + PREF_KEY_LONGPRESS_TIMEOUT); + if (pref == null) { + return; + } + pref.setInterface(new SeekBarDialogPreference.ValueProxy() { + @Override + public void writeValue(final int value, final String key) { + sp.edit().putInt(key, value).apply(); + } + + @Override + public void writeDefaultValue(final String key) { + sp.edit().remove(key).apply(); + } + + @Override + public int readValue(final String key) { + return Settings.readKeyLongpressTimeout(sp, res); + } + + @Override + public int readDefaultValue(final String key) { + return Settings.readDefaultKeyLongpressTimeout(res); + } + + @Override + public String getValueText(final int value) { + return res.getString(R.string.abbreviation_unit_milliseconds, value); + } + + @Override + public void feedbackValue(final int value) {} + }); + } + + private void setupKeyPreviewAnimationScale(final SharedPreferences sp, final Resources res, + final String prefKey, final float defaultValue) { + final SeekBarDialogPreference pref = (SeekBarDialogPreference)findPreference(prefKey); + if (pref == null) { + return; + } + pref.setInterface(new SeekBarDialogPreference.ValueProxy() { + private static final float PERCENTAGE_FLOAT = 100.0f; + + private float getValueFromPercentage(final int percentage) { + return percentage / PERCENTAGE_FLOAT; + } + + private int getPercentageFromValue(final float floatValue) { + return (int)(floatValue * PERCENTAGE_FLOAT); + } + + @Override + public void writeValue(final int value, final String key) { + sp.edit().putFloat(key, getValueFromPercentage(value)).apply(); + } + + @Override + public void writeDefaultValue(final String key) { + sp.edit().remove(key).apply(); + } + + @Override + public int readValue(final String key) { + return getPercentageFromValue( + Settings.readKeyPreviewAnimationScale(sp, key, defaultValue)); + } + + @Override + public int readDefaultValue(final String key) { + return getPercentageFromValue(defaultValue); + } + + @Override + public String getValueText(final int value) { + if (value < 0) { + return res.getString(R.string.settings_system_default); + } + return String.format("%d%%", value); + } + + @Override + public void feedbackValue(final int value) {} + }); + } + + private void setupKeyPreviewAnimationDuration(final SharedPreferences sp, final Resources res, + final String prefKey, final int defaultValue) { + final SeekBarDialogPreference pref = (SeekBarDialogPreference)findPreference(prefKey); + if (pref == null) { + return; + } + pref.setInterface(new SeekBarDialogPreference.ValueProxy() { + @Override + public void writeValue(final int value, final String key) { + sp.edit().putInt(key, value).apply(); + } + + @Override + public void writeDefaultValue(final String key) { + sp.edit().remove(key).apply(); + } + + @Override + public int readValue(final String key) { + return Settings.readKeyPreviewAnimationDuration(sp, key, defaultValue); + } + + @Override + public int readDefaultValue(final String key) { + return defaultValue; + } + + @Override + public String getValueText(final int value) { + return res.getString(R.string.abbreviation_unit_milliseconds, value); + } + + @Override + public void feedbackValue(final int value) {} + }); + } } diff --git a/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java b/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java index cd726c969..04a2ee3ce 100644 --- a/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java +++ b/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java @@ -20,7 +20,8 @@ public class NativeSuggestOptions { // Need to update suggest_options.h when you add, remove or reorder options. private static final int IS_GESTURE = 0; private static final int USE_FULL_EDIT_DISTANCE = 1; - private static final int OPTIONS_SIZE = 2; + private static final int BLOCK_OFFENSIVE_WORDS = 2; + private static final int OPTIONS_SIZE = 3; private final int[] mOptions = new int[OPTIONS_SIZE + AdditionalFeaturesSettingUtils.ADDITIONAL_FEATURES_SETTINGS_SIZE]; @@ -33,6 +34,10 @@ public class NativeSuggestOptions { setBooleanOption(USE_FULL_EDIT_DISTANCE, value); } + public void setBlockOffensiveWords(final boolean value) { + setBooleanOption(BLOCK_OFFENSIVE_WORDS, value); + } + public void setAdditionalFeaturesOptions(final int[] additionalOptions) { if (additionalOptions == null) { return; diff --git a/java/src/com/android/inputmethod/latin/settings/Settings.java b/java/src/com/android/inputmethod/latin/settings/Settings.java index df2c6907f..fb1a210bb 100644 --- a/java/src/com/android/inputmethod/latin/settings/Settings.java +++ b/java/src/com/android/inputmethod/latin/settings/Settings.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.res.Resources; +import android.os.Build; import android.preference.PreferenceManager; import android.util.Log; @@ -27,19 +28,25 @@ import com.android.inputmethod.latin.AudioAndHapticFeedbackManager; import com.android.inputmethod.latin.InputAttributes; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils; -import com.android.inputmethod.latin.utils.LocaleUtils; import com.android.inputmethod.latin.utils.ResourceUtils; import com.android.inputmethod.latin.utils.RunInLocale; import com.android.inputmethod.latin.utils.StringUtils; -import java.util.HashMap; +import java.util.Collections; import java.util.Locale; +import java.util.Set; import java.util.concurrent.locks.ReentrantLock; public final class Settings implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = Settings.class.getSimpleName(); + // Settings screens + public static final String SCREEN_INPUT = "screen_input"; + public static final String SCREEN_MULTI_LINGUAL = "screen_multi_lingual"; + public static final String SCREEN_GESTURE = "screen_gesture"; + public static final String SCREEN_CORRECTION = "screen_correction"; + public static final String SCREEN_ADVANCED = "screen_advanced"; + public static final String SCREEN_DEBUG = "screen_debug"; // In the same order as xml/prefs.xml - public static final String PREF_GENERAL_SETTINGS = "general_settings"; public static final String PREF_AUTO_CAP = "auto_cap"; public static final String PREF_VIBRATE_ON = "vibrate_on"; public static final String PREF_SOUND_ON = "sound_on"; @@ -47,33 +54,31 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang // PREF_VOICE_MODE_OBSOLETE is obsolete. Use PREF_VOICE_INPUT_KEY instead. public static final String PREF_VOICE_MODE_OBSOLETE = "voice_mode"; public static final String PREF_VOICE_INPUT_KEY = "pref_voice_input_key"; - public static final String PREF_CORRECTION_SETTINGS = "correction_settings"; public static final String PREF_EDIT_PERSONAL_DICTIONARY = "edit_personal_dictionary"; public static final String PREF_CONFIGURE_DICTIONARIES_KEY = "configure_dictionaries_key"; public static final String PREF_AUTO_CORRECTION_THRESHOLD = "auto_correction_threshold"; public static final String PREF_SHOW_SUGGESTIONS_SETTING = "show_suggestions_setting"; - public static final String PREF_MISC_SETTINGS = "misc_settings"; - public static final String PREF_LAST_USER_DICTIONARY_WRITE_TIME = - "last_user_dictionary_write_time"; - public static final String PREF_ADVANCED_SETTINGS = "pref_advanced_settings"; public static final String PREF_KEY_USE_CONTACTS_DICT = "pref_key_use_contacts_dict"; + public static final String PREF_KEY_USE_PERSONALIZED_DICTS = "pref_key_use_personalized_dicts"; public static final String PREF_KEY_USE_DOUBLE_SPACE_PERIOD = "pref_key_use_double_space_period"; public static final String PREF_BLOCK_POTENTIALLY_OFFENSIVE = "pref_key_block_potentially_offensive"; + public static final boolean ENABLE_SHOW_LANGUAGE_SWITCH_KEY_SETTINGS = + (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) + || (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT + && Build.VERSION.CODENAME.equals("REL")); 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_KEYBOARD_LAYOUT = "pref_keyboard_layout_20110916"; + public static final String PREF_KEYBOARD_THEME = "pref_keyboard_theme"; public static final String PREF_CUSTOM_INPUT_STYLES = "custom_input_styles"; + // TODO: consolidate key preview dismiss delay with the key preview animation parameters. public static final String PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY = "pref_key_preview_popup_dismiss_delay"; public static final String PREF_BIGRAM_PREDICTIONS = "next_word_prediction"; - public static final String PREF_GESTURE_SETTINGS = "gesture_typing_settings"; public static final String PREF_GESTURE_INPUT = "gesture_input"; - public static final String PREF_SLIDING_KEY_INPUT_PREVIEW = "pref_sliding_key_input_preview"; - public static final String PREF_KEY_LONGPRESS_TIMEOUT = "pref_key_longpress_timeout"; public static final String PREF_VIBRATION_DURATION_SETTINGS = "pref_vibration_duration_settings"; public static final String PREF_KEYPRESS_SOUND_VOLUME = @@ -86,9 +91,10 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static final String PREF_INPUT_LANGUAGE = "input_language"; public static final String PREF_SELECTED_LANGUAGES = "selected_languages"; - public static final String PREF_DEBUG_SETTINGS = "debug_settings"; public static final String PREF_KEY_IS_INTERNAL = "pref_key_is_internal"; + public static final String PREF_ENABLE_METRICS_LOGGING = "pref_enable_metrics_logging"; + // This preference key is deprecated. Use {@link #PREF_SHOW_LANGUAGE_SWITCH_KEY} instead. // This is being used only for the backward compatibility. private static final String PREF_SUPPRESS_LANGUAGE_SWITCH_KEY = @@ -96,14 +102,20 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang private static final String PREF_LAST_USED_PERSONALIZATION_TOKEN = "pref_last_used_personalization_token"; - public static final String PREF_SEND_FEEDBACK = "send_feedback"; - public static final String PREF_ABOUT_KEYBOARD = "about_keyboard"; + private static final String PREF_LAST_PERSONALIZATION_DICT_WIPED_TIME = + "pref_last_used_personalization_dict_wiped_time"; + private static final String PREF_CORPUS_HANDLES_FOR_PERSONALIZATION = + "pref_corpus_handles_for_personalization"; // Emoji public static final String PREF_EMOJI_RECENT_KEYS = "emoji_recent_keys"; public static final String PREF_EMOJI_CATEGORY_LAST_TYPED_ID = "emoji_category_last_typed_id"; public static final String PREF_LAST_SHOWN_EMOJI_CATEGORY_ID = "last_shown_emoji_category_id"; + private static final float UNDEFINED_PREFERENCE_VALUE_FLOAT = -1.0f; + private static final int UNDEFINED_PREFERENCE_VALUE_INT = -1; + + private Context mContext; private Resources mRes; private SharedPreferences mPrefs; private SettingsValues mSettingsValues; @@ -124,6 +136,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang } private void onCreate(final Context context) { + mContext = context; mRes = context.getResources(); mPrefs = PreferenceManager.getDefaultSharedPreferences(context); mPrefs.registerOnSharedPreferenceChangeListener(this); @@ -143,20 +156,22 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang Log.w(TAG, "onSharedPreferenceChanged called before loadSettings."); return; } - loadSettings(mSettingsValues.mLocale, mSettingsValues.mInputAttributes); + loadSettings(mContext, mSettingsValues.mLocale, mSettingsValues.mInputAttributes); } finally { mSettingsValuesLock.unlock(); } } - public void loadSettings(final Locale locale, final InputAttributes inputAttributes) { + public void loadSettings(final Context context, final Locale locale, + final InputAttributes inputAttributes) { mSettingsValuesLock.lock(); + mContext = context; try { final SharedPreferences prefs = mPrefs; final RunInLocale<SettingsValues> job = new RunInLocale<SettingsValues>() { @Override protected SettingsValues job(final Resources res) { - return new SettingsValues(prefs, locale, res, inputAttributes); + return new SettingsValues(context, prefs, res, inputAttributes); } }; mSettingsValues = job.runInLocale(mRes, locale); @@ -174,10 +189,6 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang return mSettingsValues.mIsInternal; } - public String getWordSeparators() { - return mSettingsValues.mWordSeparators; - } - public boolean isWordSeparator(final int code) { return mSettingsValues.isWordSeparator(code); } @@ -189,7 +200,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang // Accessed from the settings interface, hence public public static boolean readKeypressSoundEnabled(final SharedPreferences prefs, final Resources res) { - return prefs.getBoolean(Settings.PREF_SOUND_ON, + return prefs.getBoolean(PREF_SOUND_ON, res.getBoolean(R.bool.config_default_sound_enabled)); } @@ -209,7 +220,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static boolean readBlockPotentiallyOffensive(final SharedPreferences prefs, final Resources res) { - return prefs.getBoolean(Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE, + return prefs.getBoolean(PREF_BLOCK_POTENTIALLY_OFFENSIVE, res.getBoolean(R.bool.config_block_potentially_offensive)); } @@ -220,25 +231,24 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static boolean readGestureInputEnabled(final SharedPreferences prefs, final Resources res) { return readFromBuildConfigIfGestureInputEnabled(res) - && prefs.getBoolean(Settings.PREF_GESTURE_INPUT, true); + && prefs.getBoolean(PREF_GESTURE_INPUT, true); } public static boolean readPhraseGestureEnabled(final SharedPreferences prefs, final Resources res) { - return prefs.getBoolean(Settings.PREF_PHRASE_GESTURE_ENABLED, + return prefs.getBoolean(PREF_PHRASE_GESTURE_ENABLED, res.getBoolean(R.bool.config_default_phrase_gesture_enabled)); } - public static boolean readFromBuildConfigIfToShowKeyPreviewPopupSettingsOption( - final Resources res) { - return res.getBoolean(R.bool.config_enable_show_option_of_key_preview_popup); + public static boolean readFromBuildConfigIfToShowKeyPreviewPopupOption(final Resources res) { + return res.getBoolean(R.bool.config_enable_show_key_preview_popup_option); } public static boolean readKeyPreviewPopupEnabled(final SharedPreferences prefs, final Resources res) { final boolean defaultKeyPreviewPopup = res.getBoolean( R.bool.config_default_key_preview_popup); - if (!readFromBuildConfigIfToShowKeyPreviewPopupSettingsOption(res)) { + if (!readFromBuildConfigIfToShowKeyPreviewPopupOption(res)) { return defaultKeyPreviewPopup; } return prefs.getBoolean(PREF_POPUP_ON, defaultKeyPreviewPopup); @@ -263,28 +273,6 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang return prefs.getBoolean(PREF_SHOW_LANGUAGE_SWITCH_KEY, true); } - public static int readKeyboardThemeIndex(final SharedPreferences prefs, final Resources res) { - final String defaultThemeIndex = res.getString( - R.string.config_default_keyboard_theme_index); - final String themeIndex = prefs.getString(PREF_KEYBOARD_LAYOUT, defaultThemeIndex); - try { - return Integer.valueOf(themeIndex); - } catch (final NumberFormatException e) { - // Format error, returns default keyboard theme index. - Log.e(TAG, "Illegal keyboard theme in preference: " + themeIndex + ", default to " - + defaultThemeIndex, e); - return Integer.valueOf(defaultThemeIndex); - } - } - - public static int resetAndGetDefaultKeyboardThemeIndex(final SharedPreferences prefs, - final Resources res) { - final String defaultThemeIndex = res.getString( - R.string.config_default_keyboard_theme_index); - prefs.edit().putString(PREF_KEYBOARD_LAYOUT, defaultThemeIndex).apply(); - return Integer.valueOf(defaultThemeIndex); - } - public static String readPrefAdditionalSubtypes(final SharedPreferences prefs, final Resources res) { final String predefinedPrefSubtypes = AdditionalSubtypeUtils.createPrefSubtypes( @@ -294,24 +282,32 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static void writePrefAdditionalSubtypes(final SharedPreferences prefs, final String prefSubtypes) { - prefs.edit().putString(Settings.PREF_CUSTOM_INPUT_STYLES, prefSubtypes).apply(); + prefs.edit().putString(PREF_CUSTOM_INPUT_STYLES, prefSubtypes).apply(); } public static float readKeypressSoundVolume(final SharedPreferences prefs, final Resources res) { - final float volume = prefs.getFloat(PREF_KEYPRESS_SOUND_VOLUME, -1.0f); - return (volume >= 0) ? volume : readDefaultKeypressSoundVolume(res); + final float volume = prefs.getFloat( + PREF_KEYPRESS_SOUND_VOLUME, UNDEFINED_PREFERENCE_VALUE_FLOAT); + return (volume != UNDEFINED_PREFERENCE_VALUE_FLOAT) ? volume + : readDefaultKeypressSoundVolume(res); } + // Default keypress sound volume for unknown devices. + // The negative value means system default. + private static final String DEFAULT_KEYPRESS_SOUND_VOLUME = Float.toString(-1.0f); + public static float readDefaultKeypressSoundVolume(final Resources res) { - return Float.parseFloat( - ResourceUtils.getDeviceOverrideValue(res, R.array.keypress_volumes)); + return Float.parseFloat(ResourceUtils.getDeviceOverrideValue(res, + R.array.keypress_volumes, DEFAULT_KEYPRESS_SOUND_VOLUME)); } public static int readKeyLongpressTimeout(final SharedPreferences prefs, final Resources res) { - final int ms = prefs.getInt(PREF_KEY_LONGPRESS_TIMEOUT, -1); - return (ms >= 0) ? ms : readDefaultKeyLongpressTimeout(res); + final int milliseconds = prefs.getInt( + DebugSettings.PREF_KEY_LONGPRESS_TIMEOUT, UNDEFINED_PREFERENCE_VALUE_INT); + return (milliseconds != UNDEFINED_PREFERENCE_VALUE_INT) ? milliseconds + : readDefaultKeyLongpressTimeout(res); } public static int readDefaultKeyLongpressTimeout(final Resources res) { @@ -320,36 +316,31 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static int readKeypressVibrationDuration(final SharedPreferences prefs, final Resources res) { - final int ms = prefs.getInt(PREF_VIBRATION_DURATION_SETTINGS, -1); - return (ms >= 0) ? ms : readDefaultKeypressVibrationDuration(res); + final int milliseconds = prefs.getInt( + PREF_VIBRATION_DURATION_SETTINGS, UNDEFINED_PREFERENCE_VALUE_INT); + return (milliseconds != UNDEFINED_PREFERENCE_VALUE_INT) ? milliseconds + : readDefaultKeypressVibrationDuration(res); } - public static int readDefaultKeypressVibrationDuration(final Resources res) { - return Integer.parseInt( - ResourceUtils.getDeviceOverrideValue(res, R.array.keypress_vibration_durations)); - } + // Default keypress vibration duration for unknown devices. + // The negative value means system default. + private static final String DEFAULT_KEYPRESS_VIBRATION_DURATION = Integer.toString(-1); - public static boolean readUsabilityStudyMode(final SharedPreferences prefs) { - return prefs.getBoolean(DebugSettings.PREF_USABILITY_STUDY_MODE, true); + public static int readDefaultKeypressVibrationDuration(final Resources res) { + return Integer.parseInt(ResourceUtils.getDeviceOverrideValue(res, + R.array.keypress_vibration_durations, DEFAULT_KEYPRESS_VIBRATION_DURATION)); } - public static long readLastUserHistoryWriteTime(final SharedPreferences prefs, - final String locale) { - final String str = prefs.getString(PREF_LAST_USER_DICTIONARY_WRITE_TIME, ""); - final HashMap<String, Long> map = LocaleUtils.localeAndTimeStrToHashMap(str); - if (map.containsKey(locale)) { - return map.get(locale); - } - return 0; + public static float readKeyPreviewAnimationScale(final SharedPreferences prefs, + final String prefKey, final float defaultValue) { + final float fraction = prefs.getFloat(prefKey, UNDEFINED_PREFERENCE_VALUE_FLOAT); + return (fraction != UNDEFINED_PREFERENCE_VALUE_FLOAT) ? fraction : defaultValue; } - public static void writeLastUserHistoryWriteTime(final SharedPreferences prefs, - final String locale) { - final String oldStr = prefs.getString(PREF_LAST_USER_DICTIONARY_WRITE_TIME, ""); - final HashMap<String, Long> map = LocaleUtils.localeAndTimeStrToHashMap(oldStr); - map.put(locale, System.currentTimeMillis()); - final String newStr = LocaleUtils.localeAndTimeHashMapToStr(map); - prefs.edit().putString(PREF_LAST_USER_DICTIONARY_WRITE_TIME, newStr).apply(); + public static int readKeyPreviewAnimationDuration(final SharedPreferences prefs, + final String prefKey, final int defaultValue) { + final int milliseconds = prefs.getInt(prefKey, UNDEFINED_PREFERENCE_VALUE_INT); + return (milliseconds != UNDEFINED_PREFERENCE_VALUE_INT) ? milliseconds : defaultValue; } public static boolean readUseFullscreenMode(final Resources res) { @@ -363,35 +354,27 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang if (!enableSetupWizardByConfig) { return false; } - if (!prefs.contains(Settings.PREF_SHOW_SETUP_WIZARD_ICON)) { + if (!prefs.contains(PREF_SHOW_SETUP_WIZARD_ICON)) { final ApplicationInfo appInfo = context.getApplicationInfo(); final boolean isApplicationInSystemImage = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; // Default value return !isApplicationInSystemImage; } - return prefs.getBoolean(Settings.PREF_SHOW_SETUP_WIZARD_ICON, false); + return prefs.getBoolean(PREF_SHOW_SETUP_WIZARD_ICON, false); } public static boolean isInternal(final SharedPreferences prefs) { - return prefs.getBoolean(Settings.PREF_KEY_IS_INTERNAL, false); - } - - public static boolean readUseOnlyPersonalizationDictionaryForDebug( - final SharedPreferences prefs) { - return prefs.getBoolean( - DebugSettings.PREF_USE_ONLY_PERSONALIZATION_DICTIONARY_FOR_DEBUG, false); - } - - public static boolean readBoostPersonalizationDictionaryForDebug( - final SharedPreferences prefs) { - return prefs.getBoolean( - DebugSettings.PREF_BOOST_PERSONALIZATION_DICTIONARY_FOR_DEBUG, false); + return prefs.getBoolean(PREF_KEY_IS_INTERNAL, false); } public void writeLastUsedPersonalizationToken(byte[] token) { - final String tokenStr = StringUtils.byteArrayToHexString(token); - mPrefs.edit().putString(PREF_LAST_USED_PERSONALIZATION_TOKEN, tokenStr).apply(); + if (token == null) { + mPrefs.edit().remove(PREF_LAST_USED_PERSONALIZATION_TOKEN).apply(); + } else { + final String tokenStr = StringUtils.byteArrayToHexString(token); + mPrefs.edit().putString(PREF_LAST_USED_PERSONALIZATION_TOKEN, tokenStr).apply(); + } } public byte[] readLastUsedPersonalizationToken() { @@ -399,6 +382,23 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang return StringUtils.hexStringToByteArray(tokenStr); } + public void writeLastPersonalizationDictWipedTime(final long timestamp) { + mPrefs.edit().putLong(PREF_LAST_PERSONALIZATION_DICT_WIPED_TIME, timestamp).apply(); + } + + public long readLastPersonalizationDictGeneratedTime() { + return mPrefs.getLong(PREF_LAST_PERSONALIZATION_DICT_WIPED_TIME, 0); + } + + public void writeCorpusHandlesForPersonalization(final Set<String> corpusHandles) { + mPrefs.edit().putStringSet(PREF_CORPUS_HANDLES_FOR_PERSONALIZATION, corpusHandles).apply(); + } + + public Set<String> readCorpusHandlesForPersonalization() { + final Set<String> emptySet = Collections.emptySet(); + return mPrefs.getStringSet(PREF_CORPUS_HANDLES_FOR_PERSONALIZATION, emptySet); + } + public static void writeEmojiRecentKeys(final SharedPreferences prefs, String str) { prefs.edit().putString(PREF_EMOJI_RECENT_KEYS, str).apply(); } diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java index 5c60a7350..689f878be 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java @@ -27,16 +27,19 @@ import android.content.res.Resources; import android.media.AudioManager; import android.os.Build; import android.os.Bundle; -import android.preference.CheckBoxPreference; import android.preference.ListPreference; import android.preference.Preference; -import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; +import android.preference.TwoStatePreference; import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.dictionarypack.DictionarySettingsActivity; +import com.android.inputmethod.keyboard.KeyboardTheme; import com.android.inputmethod.latin.AudioAndHapticFeedbackManager; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.SubtypeSwitcher; @@ -48,7 +51,6 @@ import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils; import com.android.inputmethod.latin.utils.ApplicationUtils; import com.android.inputmethod.latin.utils.FeedbackUtils; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; -import com.android.inputmethod.research.ResearchLogger; import com.android.inputmethodcommon.InputMethodSettingsFragment; import java.util.TreeSet; @@ -59,14 +61,11 @@ public final class SettingsFragment extends InputMethodSettingsFragment private static final boolean DBG_USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS = false; private static final boolean USE_INTERNAL_PERSONAL_DICTIONARY_SETTIGS = DBG_USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS - || Build.VERSION.SDK_INT <= 18 /* Build.VERSION.JELLY_BEAN_MR2 */; + || Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2; - private CheckBoxPreference mVoiceInputKeyPreference; - private ListPreference mShowCorrectionSuggestionsPreference; - private ListPreference mAutoCorrectionThresholdPreference; - private ListPreference mKeyPreviewPopupDismissDelay; - // Use bigrams to predict the next word when there is no input for it yet - private CheckBoxPreference mBigramPrediction; + private static final int NO_MENU_GROUP = Menu.NONE; // We don't care about menu grouping. + private static final int MENU_FEEDBACK = Menu.FIRST; // The first menu item id and order. + private static final int MENU_ABOUT = Menu.FIRST + 1; // The second menu item id and order. private void setPreferenceEnabled(final String preferenceKey, final boolean enabled) { final Preference preference = findPreference(preferenceKey); @@ -75,6 +74,18 @@ public final class SettingsFragment extends InputMethodSettingsFragment } } + private void updateListPreferenceSummaryToCurrentValue(final String prefKey) { + // Because the "%s" summary trick of {@link ListPreference} doesn't work properly before + // KitKat, we need to update the summary programmatically. + final ListPreference listPreference = (ListPreference)findPreference(prefKey); + if (listPreference == null) { + return; + } + final CharSequence entries[] = listPreference.getEntries(); + final int entryIndex = listPreference.findIndexOfValue(listPreference.getValue()); + listPreference.setSummary(entryIndex < 0 ? null : entries[entryIndex]); + } + private static void removePreference(final String preferenceKey, final PreferenceGroup parent) { if (parent == null) { return; @@ -88,13 +99,14 @@ public final class SettingsFragment extends InputMethodSettingsFragment @Override public void onCreate(final Bundle icicle) { super.onCreate(icicle); + setHasOptionsMenu(true); setInputMethodSettingsCategoryTitle(R.string.language_selection_title); setSubtypeEnablerTitle(R.string.select_language); addPreferencesFromResource(R.xml.prefs); final PreferenceScreen preferenceScreen = getPreferenceScreen(); if (preferenceScreen != null) { preferenceScreen.setTitle( - ApplicationUtils.getAcitivityTitleResId(getActivity(), SettingsActivity.class)); + ApplicationUtils.getActivityTitleResId(getActivity(), SettingsActivity.class)); } final Resources res = getResources(); @@ -107,115 +119,99 @@ public final class SettingsFragment extends InputMethodSettingsFragment SubtypeLocaleUtils.init(context); AudioAndHapticFeedbackManager.init(context); - mVoiceInputKeyPreference = - (CheckBoxPreference) findPreference(Settings.PREF_VOICE_INPUT_KEY); - mShowCorrectionSuggestionsPreference = - (ListPreference) findPreference(Settings.PREF_SHOW_SUGGESTIONS_SETTING); final SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); prefs.registerOnSharedPreferenceChangeListener(this); - mAutoCorrectionThresholdPreference = - (ListPreference) findPreference(Settings.PREF_AUTO_CORRECTION_THRESHOLD); - mBigramPrediction = (CheckBoxPreference) findPreference(Settings.PREF_BIGRAM_PREDICTIONS); ensureConsistencyOfAutoCorrectionSettings(); - final PreferenceGroup generalSettings = - (PreferenceGroup) findPreference(Settings.PREF_GENERAL_SETTINGS); - final PreferenceGroup miscSettings = - (PreferenceGroup) findPreference(Settings.PREF_MISC_SETTINGS); - - final Preference debugSettings = findPreference(Settings.PREF_DEBUG_SETTINGS); - if (debugSettings != null) { - if (Settings.isInternal(prefs)) { - final Intent debugSettingsIntent = new Intent(Intent.ACTION_MAIN); - debugSettingsIntent.setClassName( - context.getPackageName(), DebugSettingsActivity.class.getName()); - debugSettings.setIntent(debugSettingsIntent); - } else { - miscSettings.removePreference(debugSettings); - } - } - - final Preference feedbackSettings = findPreference(Settings.PREF_SEND_FEEDBACK); - final Preference aboutSettings = findPreference(Settings.PREF_ABOUT_KEYBOARD); - if (feedbackSettings != null) { - if (FeedbackUtils.isFeedbackFormSupported()) { - feedbackSettings.setOnPreferenceClickListener(new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(final Preference pref) { - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - // Use development-only feedback mechanism - ResearchLogger.getInstance().presentFeedbackDialogFromSettings(); - } else { - FeedbackUtils.showFeedbackForm(getActivity()); - } - return true; - } - }); - aboutSettings.setTitle(FeedbackUtils.getAboutKeyboardTitleResId()); - aboutSettings.setIntent(FeedbackUtils.getAboutKeyboardIntent(getActivity())); - } else { - miscSettings.removePreference(feedbackSettings); - miscSettings.removePreference(aboutSettings); - } - } - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - // The about screen contains items that may be confusing in development-only versions. - miscSettings.removePreference(aboutSettings); + final PreferenceScreen inputScreen = + (PreferenceScreen) findPreference(Settings.SCREEN_INPUT); + final PreferenceScreen multiLingualScreen = + (PreferenceScreen) findPreference(Settings.SCREEN_MULTI_LINGUAL); + final PreferenceScreen gestureScreen = + (PreferenceScreen) findPreference(Settings.SCREEN_GESTURE); + final PreferenceScreen correctionScreen = + (PreferenceScreen) findPreference(Settings.SCREEN_CORRECTION); + final PreferenceScreen advancedScreen = + (PreferenceScreen) findPreference(Settings.SCREEN_ADVANCED); + final PreferenceScreen debugScreen = + (PreferenceScreen) findPreference(Settings.SCREEN_DEBUG); + + if (Settings.isInternal(prefs)) { + final Intent debugSettingsIntent = new Intent(Intent.ACTION_MAIN); + debugSettingsIntent.setClassName( + context.getPackageName(), DebugSettingsActivity.class.getName()); + debugScreen.setIntent(debugSettingsIntent); + } else { + advancedScreen.removePreference(debugScreen); } final boolean showVoiceKeyOption = res.getBoolean( R.bool.config_enable_show_voice_key_option); if (!showVoiceKeyOption) { - generalSettings.removePreference(mVoiceInputKeyPreference); + removePreference(Settings.PREF_VOICE_INPUT_KEY, inputScreen); } - final PreferenceGroup advancedSettings = - (PreferenceGroup) findPreference(Settings.PREF_ADVANCED_SETTINGS); if (!AudioAndHapticFeedbackManager.getInstance().hasVibrator()) { - removePreference(Settings.PREF_VIBRATE_ON, generalSettings); - removePreference(Settings.PREF_VIBRATION_DURATION_SETTINGS, advancedSettings); + removePreference(Settings.PREF_VIBRATE_ON, inputScreen); + removePreference(Settings.PREF_VIBRATION_DURATION_SETTINGS, advancedScreen); + } + if (!Settings.ENABLE_SHOW_LANGUAGE_SWITCH_KEY_SETTINGS) { + removePreference(Settings.PREF_SHOW_LANGUAGE_SWITCH_KEY, multiLingualScreen); + removePreference( + Settings.PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST, multiLingualScreen); } - mKeyPreviewPopupDismissDelay = - (ListPreference) findPreference(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY); - if (!Settings.readFromBuildConfigIfToShowKeyPreviewPopupSettingsOption(res)) { - removePreference(Settings.PREF_POPUP_ON, generalSettings); - removePreference(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY, advancedSettings); + // TODO: consolidate key preview dismiss delay with the key preview animation parameters. + if (!Settings.readFromBuildConfigIfToShowKeyPreviewPopupOption(res)) { + removePreference(Settings.PREF_POPUP_ON, inputScreen); + removePreference(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY, advancedScreen); } else { + // TODO: Cleanup this setup. + final ListPreference keyPreviewPopupDismissDelay = + (ListPreference) findPreference(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY); final String popupDismissDelayDefaultValue = Integer.toString(res.getInteger( R.integer.config_key_preview_linger_timeout)); - mKeyPreviewPopupDismissDelay.setEntries(new String[] { + keyPreviewPopupDismissDelay.setEntries(new String[] { res.getString(R.string.key_preview_popup_dismiss_no_delay), res.getString(R.string.key_preview_popup_dismiss_default_delay), }); - mKeyPreviewPopupDismissDelay.setEntryValues(new String[] { + keyPreviewPopupDismissDelay.setEntryValues(new String[] { "0", popupDismissDelayDefaultValue }); - if (null == mKeyPreviewPopupDismissDelay.getValue()) { - mKeyPreviewPopupDismissDelay.setValue(popupDismissDelayDefaultValue); + if (null == keyPreviewPopupDismissDelay.getValue()) { + keyPreviewPopupDismissDelay.setValue(popupDismissDelayDefaultValue); } - mKeyPreviewPopupDismissDelay.setEnabled( + keyPreviewPopupDismissDelay.setEnabled( Settings.readKeyPreviewPopupEnabled(prefs, res)); } if (!res.getBoolean(R.bool.config_setup_wizard_available)) { - removePreference(Settings.PREF_SHOW_SETUP_WIZARD_ICON, advancedSettings); + removePreference(Settings.PREF_SHOW_SETUP_WIZARD_ICON, advancedScreen); } - setPreferenceEnabled(Settings.PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST, - Settings.readShowsLanguageSwitchKey(prefs)); - - final PreferenceGroup textCorrectionGroup = - (PreferenceGroup) findPreference(Settings.PREF_CORRECTION_SETTINGS); final PreferenceScreen dictionaryLink = (PreferenceScreen) findPreference(Settings.PREF_CONFIGURE_DICTIONARIES_KEY); final Intent intent = dictionaryLink.getIntent(); intent.setClassName(context.getPackageName(), DictionarySettingsActivity.class.getName()); final int number = context.getPackageManager().queryIntentActivities(intent, 0).size(); if (0 >= number) { - textCorrectionGroup.removePreference(dictionaryLink); + correctionScreen.removePreference(dictionaryLink); + } + + if (ProductionFlag.IS_METRICS_LOGGING_SUPPORTED) { + final Preference enableMetricsLogging = + findPreference(Settings.PREF_ENABLE_METRICS_LOGGING); + if (enableMetricsLogging != null) { + final int applicationLabelRes = context.getApplicationInfo().labelRes; + final String applicationName = res.getString(applicationLabelRes); + final String enableMetricsLoggingTitle = res.getString( + R.string.enable_metrics_logging, applicationName); + enableMetricsLogging.setTitle(enableMetricsLoggingTitle); + } + } else { + removePreference(Settings.PREF_ENABLE_METRICS_LOGGING, advancedScreen); } final Preference editPersonalDictionary = @@ -229,12 +225,11 @@ public final class SettingsFragment extends InputMethodSettingsFragment } if (!Settings.readFromBuildConfigIfGestureInputEnabled(res)) { - removePreference(Settings.PREF_GESTURE_SETTINGS, getPreferenceScreen()); + getPreferenceScreen().removePreference(gestureScreen); } AdditionalFeaturesSettingUtils.addAdditionalFeaturesPreferences(context, this); - setupKeyLongpressTimeoutSettings(prefs, res); setupKeypressVibrationDurationSettings(prefs, res); setupKeypressSoundVolumeSettings(prefs, res); refreshEnablingsOfKeypressSoundAndVibrationSettings(prefs, res); @@ -243,20 +238,45 @@ public final class SettingsFragment extends InputMethodSettingsFragment @Override public void onResume() { super.onResume(); - final boolean isShortcutImeEnabled = SubtypeSwitcher.getInstance().isShortcutImeEnabled(); - if (!isShortcutImeEnabled) { - getPreferenceScreen().removePreference(mVoiceInputKeyPreference); - } final SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); - final CheckBoxPreference showSetupWizardIcon = - (CheckBoxPreference)findPreference(Settings.PREF_SHOW_SETUP_WIZARD_ICON); + final Resources res = getResources(); + final Preference voiceInputKeyOption = findPreference(Settings.PREF_VOICE_INPUT_KEY); + if (voiceInputKeyOption != null) { + final boolean isShortcutImeEnabled = SubtypeSwitcher.getInstance() + .isShortcutImeEnabled(); + voiceInputKeyOption.setEnabled(isShortcutImeEnabled); + voiceInputKeyOption.setSummary(isShortcutImeEnabled ? null + : res.getText(R.string.voice_input_disabled_summary)); + } + final TwoStatePreference showSetupWizardIcon = + (TwoStatePreference)findPreference(Settings.PREF_SHOW_SETUP_WIZARD_ICON); if (showSetupWizardIcon != null) { showSetupWizardIcon.setChecked(Settings.readShowSetupWizardIcon(prefs, getActivity())); } - updateShowCorrectionSuggestionsSummary(); - updateKeyPreviewPopupDelaySummary(); - updateColorSchemeSummary(prefs, getResources()); - updateCustomInputStylesSummary(); + updateListPreferenceSummaryToCurrentValue(Settings.PREF_SHOW_SUGGESTIONS_SETTING); + updateListPreferenceSummaryToCurrentValue(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY); + final ListPreference keyboardThemePref = (ListPreference)findPreference( + Settings.PREF_KEYBOARD_THEME); + if (keyboardThemePref != null) { + final KeyboardTheme keyboardTheme = KeyboardTheme.getKeyboardTheme(prefs); + final String value = Integer.toString(keyboardTheme.mThemeId); + final CharSequence entries[] = keyboardThemePref.getEntries(); + final int entryIndex = keyboardThemePref.findIndexOfValue(value); + keyboardThemePref.setSummary(entryIndex < 0 ? null : entries[entryIndex]); + keyboardThemePref.setValue(value); + } + updateCustomInputStylesSummary(prefs, res); + } + + @Override + public void onPause() { + super.onPause(); + final SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); + final ListPreference keyboardThemePref = (ListPreference)findPreference( + Settings.PREF_KEYBOARD_THEME); + if (keyboardThemePref != null) { + KeyboardTheme.saveKeyboardThemeId(keyboardThemePref.getValue(), prefs); + } } @Override @@ -280,57 +300,30 @@ public final class SettingsFragment extends InputMethodSettingsFragment if (key.equals(Settings.PREF_POPUP_ON)) { setPreferenceEnabled(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY, Settings.readKeyPreviewPopupEnabled(prefs, res)); - } else if (key.equals(Settings.PREF_SHOW_LANGUAGE_SWITCH_KEY)) { - setPreferenceEnabled(Settings.PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST, - Settings.readShowsLanguageSwitchKey(prefs)); } else if (key.equals(Settings.PREF_SHOW_SETUP_WIZARD_ICON)) { LauncherIconVisibilityManager.updateSetupWizardIconVisibility(getActivity()); } ensureConsistencyOfAutoCorrectionSettings(); - updateShowCorrectionSuggestionsSummary(); - updateKeyPreviewPopupDelaySummary(); - updateColorSchemeSummary(prefs, res); + updateListPreferenceSummaryToCurrentValue(Settings.PREF_SHOW_SUGGESTIONS_SETTING); + updateListPreferenceSummaryToCurrentValue(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY); + updateListPreferenceSummaryToCurrentValue(Settings.PREF_KEYBOARD_THEME); refreshEnablingsOfKeypressSoundAndVibrationSettings(prefs, getResources()); } private void ensureConsistencyOfAutoCorrectionSettings() { final String autoCorrectionOff = getResources().getString( R.string.auto_correction_threshold_mode_index_off); - final String currentSetting = mAutoCorrectionThresholdPreference.getValue(); - mBigramPrediction.setEnabled(!currentSetting.equals(autoCorrectionOff)); + final ListPreference autoCorrectionThresholdPref = (ListPreference)findPreference( + Settings.PREF_AUTO_CORRECTION_THRESHOLD); + final String currentSetting = autoCorrectionThresholdPref.getValue(); + setPreferenceEnabled( + Settings.PREF_BIGRAM_PREDICTIONS, !currentSetting.equals(autoCorrectionOff)); } - private void updateShowCorrectionSuggestionsSummary() { - mShowCorrectionSuggestionsPreference.setSummary( - getResources().getStringArray(R.array.prefs_suggestion_visibilities) - [mShowCorrectionSuggestionsPreference.findIndexOfValue( - mShowCorrectionSuggestionsPreference.getValue())]); - } - - private void updateColorSchemeSummary(final SharedPreferences prefs, final Resources res) { - // Because the "%s" summary trick of {@link ListPreference} doesn't work properly before - // KitKat, we need to update the summary by code. - final Preference preference = findPreference(Settings.PREF_KEYBOARD_LAYOUT); - if (!(preference instanceof ListPreference)) { - Log.w(TAG, "Can't find Keyboard Color Scheme preference"); - return; - } - final ListPreference colorSchemePreference = (ListPreference)preference; - final int themeIndex = Settings.readKeyboardThemeIndex(prefs, res); - int entryIndex = colorSchemePreference.findIndexOfValue(Integer.toString(themeIndex)); - if (entryIndex < 0) { - final int defaultThemeIndex = Settings.resetAndGetDefaultKeyboardThemeIndex(prefs, res); - entryIndex = colorSchemePreference.findIndexOfValue( - Integer.toString(defaultThemeIndex)); - } - colorSchemePreference.setSummary(colorSchemePreference.getEntries()[entryIndex]); - } - - private void updateCustomInputStylesSummary() { + private void updateCustomInputStylesSummary(final SharedPreferences prefs, + final Resources res) { final PreferenceScreen customInputStyles = (PreferenceScreen)findPreference(Settings.PREF_CUSTOM_INPUT_STYLES); - final SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); - final Resources res = getResources(); final String prefSubtype = Settings.readPrefAdditionalSubtypes(prefs, res); final InputMethodSubtype[] subtypes = AdditionalSubtypeUtils.createAdditionalSubtypesArray(prefSubtype); @@ -342,13 +335,6 @@ public final class SettingsFragment extends InputMethodSettingsFragment customInputStyles.setSummary(styles); } - private void updateKeyPreviewPopupDelaySummary() { - final ListPreference lp = mKeyPreviewPopupDismissDelay; - final CharSequence[] entries = lp.getEntries(); - if (entries == null || entries.length <= 0) return; - lp.setSummary(entries[lp.findIndexOfValue(lp.getValue())]); - } - private void refreshEnablingsOfKeypressSoundAndVibrationSettings( final SharedPreferences sp, final Resources res) { setPreferenceEnabled(Settings.PREF_VIBRATION_DURATION_SETTINGS, @@ -400,44 +386,6 @@ public final class SettingsFragment extends InputMethodSettingsFragment }); } - private void setupKeyLongpressTimeoutSettings(final SharedPreferences sp, - final Resources res) { - final SeekBarDialogPreference pref = (SeekBarDialogPreference)findPreference( - Settings.PREF_KEY_LONGPRESS_TIMEOUT); - if (pref == null) { - return; - } - pref.setInterface(new SeekBarDialogPreference.ValueProxy() { - @Override - public void writeValue(final int value, final String key) { - sp.edit().putInt(key, value).apply(); - } - - @Override - public void writeDefaultValue(final String key) { - sp.edit().remove(key).apply(); - } - - @Override - public int readValue(final String key) { - return Settings.readKeyLongpressTimeout(sp, res); - } - - @Override - public int readDefaultValue(final String key) { - return Settings.readDefaultKeyLongpressTimeout(res); - } - - @Override - public String getValueText(final int value) { - return res.getString(R.string.abbreviation_unit_milliseconds, value); - } - - @Override - public void feedbackValue(final int value) {} - }); - } - private void setupKeypressSoundVolumeSettings(final SharedPreferences sp, final Resources res) { final SeekBarDialogPreference pref = (SeekBarDialogPreference)findPreference( Settings.PREF_KEYPRESS_SOUND_VOLUME); @@ -515,4 +463,33 @@ public final class SettingsFragment extends InputMethodSettingsFragment userDictionaryPreference.setFragment(UserDictionaryList.class.getName()); } } + + @Override + public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { + if (FeedbackUtils.isFeedbackFormSupported()) { + menu.add(NO_MENU_GROUP, MENU_FEEDBACK /* itemId */, MENU_FEEDBACK /* order */, + R.string.send_feedback); + } + final int aboutResId = FeedbackUtils.getAboutKeyboardTitleResId(); + if (aboutResId != 0) { + menu.add(NO_MENU_GROUP, MENU_ABOUT /* itemId */, MENU_ABOUT /* order */, aboutResId); + } + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + final int itemId = item.getItemId(); + if (itemId == MENU_FEEDBACK) { + FeedbackUtils.showFeedbackForm(getActivity()); + return true; + } + if (itemId == MENU_ABOUT) { + final Intent aboutIntent = FeedbackUtils.getAboutKeyboardIntent(getActivity()); + if (aboutIntent != null) { + startActivity(aboutIntent); + return true; + } + } + return super.onOptionsItemSelected(item); + } } diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java index f331c78e5..8de5fed07 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java @@ -16,27 +16,23 @@ package com.android.inputmethod.latin.settings; +import android.content.Context; import android.content.SharedPreferences; +import android.content.pm.PackageInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.util.Log; import android.view.inputmethod.EditorInfo; -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.keyboard.internal.KeySpecParser; -import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.Dictionary; +import com.android.inputmethod.compat.AppWorkaroundsUtils; import com.android.inputmethod.latin.InputAttributes; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.RichInputMethodManager; import com.android.inputmethod.latin.SubtypeSwitcher; -import com.android.inputmethod.latin.SuggestedWords; -import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; -import com.android.inputmethod.latin.utils.CollectionUtils; -import com.android.inputmethod.latin.utils.InputTypeUtils; -import com.android.inputmethod.latin.utils.StringUtils; +import com.android.inputmethod.latin.utils.AsyncResultHolder; +import com.android.inputmethod.latin.utils.ResourceUtils; +import com.android.inputmethod.latin.utils.TargetPackageInfoGetterTask; -import java.util.ArrayList; import java.util.Arrays; import java.util.Locale; @@ -50,27 +46,23 @@ public final class SettingsValues { // Float.NEGATIVE_INFINITE and Float.MAX_VALUE. Currently used for auto-correction settings. private static final String FLOAT_MAX_VALUE_MARKER_STRING = "floatMaxValue"; private static final String FLOAT_NEGATIVE_INFINITY_MARKER_STRING = "floatNegativeInfinity"; + private static final int TIMEOUT_TO_GET_TARGET_PACKAGE = 5; // seconds // From resources: + public final SpacingAndPunctuations mSpacingAndPunctuations; public final int mDelayUpdateOldSuggestions; - public final int[] mSymbolsPrecededBySpace; - public final int[] mSymbolsFollowedBySpace; - public final int[] mWordConnectors; - public final SuggestedWords mSuggestPuncList; - public final String mWordSeparators; - public final int mSentenceSeparator; - public final CharSequence mHintToSaveText; - public final boolean mCurrentLanguageHasSpaces; + public final long mDoubleSpacePeriodTimeout; // From preferences, in the same order as xml/prefs.xml: public final boolean mAutoCap; public final boolean mVibrateOn; public final boolean mSoundOn; public final boolean mKeyPreviewPopupOn; - private final boolean mShowsVoiceInputKey; + public final boolean mShowsVoiceInputKey; public final boolean mIncludesOtherImesInLanguageSwitchList; public final boolean mShowsLanguageSwitchKey; public final boolean mUseContactsDict; + public final boolean mUsePersonalizedDicts; public final boolean mUseDoubleSpacePeriod; public final boolean mBlockPotentiallyOffensive; // Use bigrams to predict the next word when there is no input for it yet @@ -82,6 +74,7 @@ public final class SettingsValues { public final boolean mPhraseGestureEnabled; public final int mKeyLongpressTimeout; public final Locale mLocale; + public final boolean mEnableMetricsLogging; // From the input box public final InputAttributes mInputAttributes; @@ -92,10 +85,11 @@ public final class SettingsValues { public final int mKeyPreviewPopupDismissDelay; private final boolean mAutoCorrectEnabled; public final float mAutoCorrectionThreshold; - public final boolean mCorrectionEnabled; + // TODO: Rename this to mAutoCorrectionEnabledPerUserSettings. + public final boolean mAutoCorrectionEnabled; public final int mSuggestionVisibility; - public final boolean mBoostPersonalizationDictionaryForDebug; - public final boolean mUseOnlyPersonalizationDictionaryForDebug; + public final int mDisplayOrientation; + private final AsyncResultHolder<AppWorkaroundsUtils> mAppWorkarounds; // Setting values for additional features public final int[] mAdditionalFeaturesSettingValues = @@ -103,32 +97,22 @@ public final class SettingsValues { // Debug settings public final boolean mIsInternal; + public final int mKeyPreviewShowUpDuration; + public final int mKeyPreviewDismissDuration; + public final float mKeyPreviewShowUpStartScale; + public final float mKeyPreviewDismissEndScale; - public SettingsValues(final SharedPreferences prefs, final Locale locale, final Resources res, + public SettingsValues(final Context context, final SharedPreferences prefs, final Resources res, final InputAttributes inputAttributes) { - mLocale = locale; + mLocale = res.getConfiguration().locale; // Get the resources mDelayUpdateOldSuggestions = res.getInteger(R.integer.config_delay_update_old_suggestions); - mSymbolsPrecededBySpace = - StringUtils.toCodePointArray(res.getString(R.string.symbols_preceded_by_space)); - Arrays.sort(mSymbolsPrecededBySpace); - mSymbolsFollowedBySpace = - StringUtils.toCodePointArray(res.getString(R.string.symbols_followed_by_space)); - Arrays.sort(mSymbolsFollowedBySpace); - mWordConnectors = - StringUtils.toCodePointArray(res.getString(R.string.symbols_word_connectors)); - Arrays.sort(mWordConnectors); - final String[] suggestPuncsSpec = KeySpecParser.splitKeySpecs(res.getString( - R.string.suggested_punctuations)); - mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec); - mWordSeparators = res.getString(R.string.symbols_word_separators); - mSentenceSeparator = res.getInteger(R.integer.sentence_separator); - mHintToSaveText = res.getText(R.string.hint_add_to_dictionary); - mCurrentLanguageHasSpaces = res.getBoolean(R.bool.current_language_has_spaces); + mSpacingAndPunctuations = new SpacingAndPunctuations(res); // Store the input attributes if (null == inputAttributes) { - mInputAttributes = new InputAttributes(null, false /* isFullscreenMode */); + mInputAttributes = new InputAttributes( + null, false /* isFullscreenMode */, context.getPackageName()); } else { mInputAttributes = inputAttributes; } @@ -139,20 +123,26 @@ public final class SettingsValues { mSoundOn = Settings.readKeypressSoundEnabled(prefs, res); mKeyPreviewPopupOn = Settings.readKeyPreviewPopupEnabled(prefs, res); mSlidingKeyInputPreviewEnabled = prefs.getBoolean( - Settings.PREF_SLIDING_KEY_INPUT_PREVIEW, true); - mShowsVoiceInputKey = needsToShowVoiceInputKey(prefs, res); + DebugSettings.PREF_SLIDING_KEY_INPUT_PREVIEW, true); + mShowsVoiceInputKey = needsToShowVoiceInputKey(prefs, res) + && mInputAttributes.mShouldShowVoiceInputKey + && SubtypeSwitcher.getInstance().isShortcutImeEnabled(); final String autoCorrectionThresholdRawValue = prefs.getString( Settings.PREF_AUTO_CORRECTION_THRESHOLD, res.getString(R.string.auto_correction_threshold_mode_index_modest)); - mIncludesOtherImesInLanguageSwitchList = prefs.getBoolean( - Settings.PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST, false); - mShowsLanguageSwitchKey = Settings.readShowsLanguageSwitchKey(prefs); + mIncludesOtherImesInLanguageSwitchList = Settings.ENABLE_SHOW_LANGUAGE_SWITCH_KEY_SETTINGS + ? prefs.getBoolean(Settings.PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST, false) + : true /* forcibly */; + mShowsLanguageSwitchKey = Settings.ENABLE_SHOW_LANGUAGE_SWITCH_KEY_SETTINGS + ? Settings.readShowsLanguageSwitchKey(prefs) : true /* forcibly */; mUseContactsDict = prefs.getBoolean(Settings.PREF_KEY_USE_CONTACTS_DICT, true); + mUsePersonalizedDicts = prefs.getBoolean(Settings.PREF_KEY_USE_PERSONALIZED_DICTS, true); mUseDoubleSpacePeriod = prefs.getBoolean(Settings.PREF_KEY_USE_DOUBLE_SPACE_PERIOD, true); mBlockPotentiallyOffensive = Settings.readBlockPotentiallyOffensive(prefs, res); mAutoCorrectEnabled = Settings.readAutoCorrectEnabled(autoCorrectionThresholdRawValue, res); mBigramPredictionEnabled = readBigramPredictionEnabled(prefs, res); - + mDoubleSpacePeriodTimeout = res.getInteger(R.integer.config_double_space_period_timeout); + mEnableMetricsLogging = prefs.getBoolean(Settings.PREF_ENABLE_METRICS_LOGGING, true); // Compute other readable settings mKeyLongpressTimeout = Settings.readKeyLongpressTimeout(prefs, res); mKeypressVibrationDuration = Settings.readKeypressVibrationDuration(prefs, res); @@ -165,7 +155,7 @@ public final class SettingsValues { mGestureFloatingPreviewTextEnabled = prefs.getBoolean( Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT, true); mPhraseGestureEnabled = Settings.readPhraseGestureEnabled(prefs, res); - mCorrectionEnabled = mAutoCorrectEnabled && !mInputAttributes.mInputTypeNoAutoCorrect; + mAutoCorrectionEnabled = mAutoCorrectEnabled && !mInputAttributes.mInputTypeNoAutoCorrect; final String showSuggestionsSetting = prefs.getString( Settings.PREF_SHOW_SUGGESTIONS_SETTING, res.getString(R.string.prefs_suggestion_visibility_default_value)); @@ -173,111 +163,74 @@ public final class SettingsValues { AdditionalFeaturesSettingUtils.readAdditionalFeaturesPreferencesIntoArray( prefs, mAdditionalFeaturesSettingValues); mIsInternal = Settings.isInternal(prefs); - mBoostPersonalizationDictionaryForDebug = - Settings.readBoostPersonalizationDictionaryForDebug(prefs); - mUseOnlyPersonalizationDictionaryForDebug = - Settings.readUseOnlyPersonalizationDictionaryForDebug(prefs); - } - - // Only for tests - private SettingsValues(final Locale locale) { - // TODO: locale is saved, but not used yet. May have to change this if tests require. - mLocale = locale; - mDelayUpdateOldSuggestions = 0; - mSymbolsPrecededBySpace = new int[] { '(', '[', '{', '&' }; - Arrays.sort(mSymbolsPrecededBySpace); - mSymbolsFollowedBySpace = new int[] { '.', ',', ';', ':', '!', '?', ')', ']', '}', '&' }; - Arrays.sort(mSymbolsFollowedBySpace); - mWordConnectors = new int[] { '\'', '-' }; - Arrays.sort(mWordConnectors); - mSentenceSeparator = Constants.CODE_PERIOD; - final String[] suggestPuncsSpec = new String[] { "!", "?", ",", ":", ";" }; - mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec); - mWordSeparators = "&\t \n()[]{}*&<>+=|.,;:!?/_\""; - mHintToSaveText = "Touch again to save"; - mCurrentLanguageHasSpaces = true; - mInputAttributes = new InputAttributes(null, false /* isFullscreenMode */); - mAutoCap = true; - mVibrateOn = true; - mSoundOn = true; - mKeyPreviewPopupOn = true; - mSlidingKeyInputPreviewEnabled = true; - mShowsVoiceInputKey = true; - mIncludesOtherImesInLanguageSwitchList = false; - mShowsLanguageSwitchKey = true; - mUseContactsDict = true; - mUseDoubleSpacePeriod = true; - mBlockPotentiallyOffensive = true; - mAutoCorrectEnabled = true; - mBigramPredictionEnabled = true; - mKeyLongpressTimeout = 300; - mKeypressVibrationDuration = 5; - mKeypressSoundVolume = 1; - mKeyPreviewPopupDismissDelay = 70; - mAutoCorrectionThreshold = 1; - mGestureInputEnabled = true; - mGestureTrailEnabled = true; - mGestureFloatingPreviewTextEnabled = true; - mPhraseGestureEnabled = true; - mCorrectionEnabled = mAutoCorrectEnabled && !mInputAttributes.mInputTypeNoAutoCorrect; - mSuggestionVisibility = 0; - mIsInternal = false; - mBoostPersonalizationDictionaryForDebug = false; - mUseOnlyPersonalizationDictionaryForDebug = false; - } - - @UsedForTesting - public static SettingsValues makeDummySettingsValuesForTest(final Locale locale) { - return new SettingsValues(locale); + mKeyPreviewShowUpDuration = Settings.readKeyPreviewAnimationDuration( + prefs, DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_DURATION, + res.getInteger(R.integer.config_key_preview_show_up_duration)); + mKeyPreviewDismissDuration = Settings.readKeyPreviewAnimationDuration( + prefs, DebugSettings.PREF_KEY_PREVIEW_DISMISS_DURATION, + res.getInteger(R.integer.config_key_preview_dismiss_duration)); + mKeyPreviewShowUpStartScale = Settings.readKeyPreviewAnimationScale( + prefs, DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_START_SCALE, + ResourceUtils.getFloatFromFraction( + res, R.fraction.config_key_preview_show_up_start_scale)); + mKeyPreviewDismissEndScale = Settings.readKeyPreviewAnimationScale( + prefs, DebugSettings.PREF_KEY_PREVIEW_DISMISS_END_SCALE, + ResourceUtils.getFloatFromFraction( + res, R.fraction.config_key_preview_dismiss_end_scale)); + mDisplayOrientation = res.getConfiguration().orientation; + mAppWorkarounds = new AsyncResultHolder<>(); + final PackageInfo packageInfo = TargetPackageInfoGetterTask.getCachedPackageInfo( + mInputAttributes.mTargetApplicationPackageName); + if (null != packageInfo) { + mAppWorkarounds.set(new AppWorkaroundsUtils(packageInfo)); + } else { + new TargetPackageInfoGetterTask(context, mAppWorkarounds) + .execute(mInputAttributes.mTargetApplicationPackageName); + } } public boolean isApplicationSpecifiedCompletionsOn() { return mInputAttributes.mApplicationSpecifiedCompletionOn; } - public boolean isSuggestionsRequested(final int displayOrientation) { - return mInputAttributes.mIsSettingsSuggestionStripOn - && (mCorrectionEnabled - || isSuggestionStripVisibleInOrientation(displayOrientation)); + // TODO: Rename this to needsToLookupSuggestions(). + public boolean isSuggestionsRequested() { + return mInputAttributes.mShouldShowSuggestions + && (mAutoCorrectionEnabled + || isCurrentOrientationAllowingSuggestionsPerUserSettings()); } - public boolean isSuggestionStripVisibleInOrientation(final int orientation) { + public boolean isCurrentOrientationAllowingSuggestionsPerUserSettings() { return (mSuggestionVisibility == SUGGESTION_VISIBILITY_SHOW_VALUE) || (mSuggestionVisibility == SUGGESTION_VISIBILITY_SHOW_ONLY_PORTRAIT_VALUE - && orientation == Configuration.ORIENTATION_PORTRAIT); + && mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT); } public boolean isWordSeparator(final int code) { - return mWordSeparators.contains(String.valueOf((char)code)); + return mSpacingAndPunctuations.isWordSeparator(code); } public boolean isWordConnector(final int code) { - return Arrays.binarySearch(mWordConnectors, code) >= 0; + return mSpacingAndPunctuations.isWordConnector(code); } public boolean isWordCodePoint(final int code) { - return Character.isLetter(code) || isWordConnector(code); + return Character.isLetter(code) || isWordConnector(code) + || Character.COMBINING_SPACING_MARK == Character.getType(code); } public boolean isUsuallyPrecededBySpace(final int code) { - return Arrays.binarySearch(mSymbolsPrecededBySpace, code) >= 0; + return mSpacingAndPunctuations.isUsuallyPrecededBySpace(code); } public boolean isUsuallyFollowedBySpace(final int code) { - return Arrays.binarySearch(mSymbolsFollowedBySpace, code) >= 0; + return mSpacingAndPunctuations.isUsuallyFollowedBySpace(code); } public boolean shouldInsertSpacesAutomatically() { return mInputAttributes.mShouldInsertSpacesAutomatically; } - public boolean isVoiceKeyEnabled(final EditorInfo editorInfo) { - final boolean shortcutImeEnabled = SubtypeSwitcher.getInstance().isShortcutImeEnabled(); - final int inputType = (editorInfo != null) ? editorInfo.inputType : 0; - return shortcutImeEnabled && mShowsVoiceInputKey - && !InputTypeUtils.isPasswordInputType(inputType); - } - public boolean isLanguageSwitchKeyEnabled() { if (!mShowsLanguageSwitchKey) { return false; @@ -294,25 +247,20 @@ public final class SettingsValues { return mInputAttributes.isSameInputType(editorInfo); } - // Helper functions to create member values. - private static SuggestedWords createSuggestPuncList(final String[] puncs) { - final ArrayList<SuggestedWordInfo> puncList = CollectionUtils.newArrayList(); - if (puncs != null) { - for (final String puncSpec : puncs) { - // TODO: Stop using KeySpceParser.getLabel(). - puncList.add(new SuggestedWordInfo(KeySpecParser.getLabel(puncSpec), - SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_HARDCODED, - Dictionary.DICTIONARY_HARDCODED, - SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, - SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); - } - } - return new SuggestedWords(puncList, - false /* typedWordValid */, - false /* hasAutoCorrectionCandidate */, - true /* isPunctuationSuggestions */, - false /* isObsoleteSuggestions */, - false /* isPrediction */); + public boolean hasSameOrientation(final Configuration configuration) { + return mDisplayOrientation == configuration.orientation; + } + + public boolean isBeforeJellyBean() { + final AppWorkaroundsUtils appWorkaroundUtils + = mAppWorkarounds.get(null, TIMEOUT_TO_GET_TARGET_PACKAGE); + return null == appWorkaroundUtils ? false : appWorkaroundUtils.isBeforeJellyBean(); + } + + public boolean isBrokenByRecorrection() { + final AppWorkaroundsUtils appWorkaroundUtils + = mAppWorkarounds.get(null, TIMEOUT_TO_GET_TARGET_PACKAGE); + return null == appWorkaroundUtils ? false : appWorkaroundUtils.isBrokenByRecorrection(); } private static final int SUGGESTION_VISIBILITY_SHOW_VALUE = @@ -350,7 +298,7 @@ public final class SettingsValues { // When autoCorrectionThreshold is greater than 1.0, it's like auto correction is off. final float autoCorrectionThreshold; try { - final int arrayIndex = Integer.valueOf(currentAutoCorrectionSetting); + final int arrayIndex = Integer.parseInt(currentAutoCorrectionSetting); if (arrayIndex >= 0 && arrayIndex < autoCorrectionThresholdValues.length) { final String val = autoCorrectionThresholdValues[arrayIndex]; if (FLOAT_MAX_VALUE_MARKER_STRING.equals(val)) { @@ -374,17 +322,101 @@ public final class SettingsValues { return autoCorrectionThreshold; } - private static boolean needsToShowVoiceInputKey(SharedPreferences prefs, Resources res) { - final String voiceModeMain = res.getString(R.string.voice_mode_main); - final String voiceMode = prefs.getString(Settings.PREF_VOICE_MODE_OBSOLETE, voiceModeMain); - final boolean showsVoiceInputKey = voiceMode == null || voiceMode.equals(voiceModeMain); - if (!showsVoiceInputKey) { - // Migrate settings from PREF_VOICE_MODE_OBSOLETE to PREF_VOICE_INPUT_KEY - // Set voiceModeMain as a value of obsolete voice mode settings. - prefs.edit().putString(Settings.PREF_VOICE_MODE_OBSOLETE, voiceModeMain).apply(); - // Disable voice input key. - prefs.edit().putBoolean(Settings.PREF_VOICE_INPUT_KEY, false).apply(); + private static boolean needsToShowVoiceInputKey(final SharedPreferences prefs, + final Resources res) { + // Migrate preference from {@link Settings#PREF_VOICE_MODE_OBSOLETE} to + // {@link Settings#PREF_VOICE_INPUT_KEY}. + if (prefs.contains(Settings.PREF_VOICE_MODE_OBSOLETE)) { + final String voiceModeMain = res.getString(R.string.voice_mode_main); + final String voiceMode = prefs.getString( + Settings.PREF_VOICE_MODE_OBSOLETE, voiceModeMain); + final boolean shouldShowVoiceInputKey = voiceModeMain.equals(voiceMode); + prefs.edit() + .putBoolean(Settings.PREF_VOICE_INPUT_KEY, shouldShowVoiceInputKey) + // Remove the obsolete preference if exists. + .remove(Settings.PREF_VOICE_MODE_OBSOLETE) + .apply(); } return prefs.getBoolean(Settings.PREF_VOICE_INPUT_KEY, true); } + + public String dump() { + final StringBuilder sb = new StringBuilder("Current settings :"); + sb.append("\n mSpacingAndPunctuations = "); + sb.append("" + mSpacingAndPunctuations.dump()); + sb.append("\n mDelayUpdateOldSuggestions = "); + sb.append("" + mDelayUpdateOldSuggestions); + sb.append("\n mAutoCap = "); + sb.append("" + mAutoCap); + sb.append("\n mVibrateOn = "); + sb.append("" + mVibrateOn); + sb.append("\n mSoundOn = "); + sb.append("" + mSoundOn); + sb.append("\n mKeyPreviewPopupOn = "); + sb.append("" + mKeyPreviewPopupOn); + sb.append("\n mShowsVoiceInputKey = "); + sb.append("" + mShowsVoiceInputKey); + sb.append("\n mIncludesOtherImesInLanguageSwitchList = "); + sb.append("" + mIncludesOtherImesInLanguageSwitchList); + sb.append("\n mShowsLanguageSwitchKey = "); + sb.append("" + mShowsLanguageSwitchKey); + sb.append("\n mUseContactsDict = "); + sb.append("" + mUseContactsDict); + sb.append("\n mUsePersonalizedDicts = "); + sb.append("" + mUsePersonalizedDicts); + sb.append("\n mUseDoubleSpacePeriod = "); + sb.append("" + mUseDoubleSpacePeriod); + sb.append("\n mBlockPotentiallyOffensive = "); + sb.append("" + mBlockPotentiallyOffensive); + sb.append("\n mBigramPredictionEnabled = "); + sb.append("" + mBigramPredictionEnabled); + sb.append("\n mGestureInputEnabled = "); + sb.append("" + mGestureInputEnabled); + sb.append("\n mGestureTrailEnabled = "); + sb.append("" + mGestureTrailEnabled); + sb.append("\n mGestureFloatingPreviewTextEnabled = "); + sb.append("" + mGestureFloatingPreviewTextEnabled); + sb.append("\n mSlidingKeyInputPreviewEnabled = "); + sb.append("" + mSlidingKeyInputPreviewEnabled); + sb.append("\n mPhraseGestureEnabled = "); + sb.append("" + mPhraseGestureEnabled); + sb.append("\n mKeyLongpressTimeout = "); + sb.append("" + mKeyLongpressTimeout); + sb.append("\n mLocale = "); + sb.append("" + mLocale); + sb.append("\n mInputAttributes = "); + sb.append("" + mInputAttributes); + sb.append("\n mKeypressVibrationDuration = "); + sb.append("" + mKeypressVibrationDuration); + sb.append("\n mKeypressSoundVolume = "); + sb.append("" + mKeypressSoundVolume); + sb.append("\n mKeyPreviewPopupDismissDelay = "); + sb.append("" + mKeyPreviewPopupDismissDelay); + sb.append("\n mAutoCorrectEnabled = "); + sb.append("" + mAutoCorrectEnabled); + sb.append("\n mAutoCorrectionThreshold = "); + sb.append("" + mAutoCorrectionThreshold); + sb.append("\n mAutoCorrectionEnabled = "); + sb.append("" + mAutoCorrectionEnabled); + sb.append("\n mSuggestionVisibility = "); + sb.append("" + mSuggestionVisibility); + sb.append("\n mDisplayOrientation = "); + sb.append("" + mDisplayOrientation); + sb.append("\n mAppWorkarounds = "); + final AppWorkaroundsUtils awu = mAppWorkarounds.get(null, 0); + sb.append("" + (null == awu ? "null" : awu.toString())); + sb.append("\n mAdditionalFeaturesSettingValues = "); + sb.append("" + Arrays.toString(mAdditionalFeaturesSettingValues)); + sb.append("\n mIsInternal = "); + sb.append("" + mIsInternal); + sb.append("\n mKeyPreviewShowUpDuration = "); + sb.append("" + mKeyPreviewShowUpDuration); + sb.append("\n mKeyPreviewDismissDuration = "); + sb.append("" + mKeyPreviewDismissDuration); + sb.append("\n mKeyPreviewShowUpStartScale = "); + sb.append("" + mKeyPreviewShowUpStartScale); + sb.append("\n mKeyPreviewDismissEndScale = "); + sb.append("" + mKeyPreviewDismissEndScale); + return sb.toString(); + } } diff --git a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java new file mode 100644 index 000000000..b8d2a2248 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.settings; + +import android.content.res.Resources; + +import com.android.inputmethod.keyboard.internal.MoreKeySpec; +import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.PunctuationSuggestions; +import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.utils.StringUtils; + +import java.util.Arrays; +import java.util.Locale; + +public final class SpacingAndPunctuations { + private final int[] mSortedSymbolsPrecededBySpace; + private final int[] mSortedSymbolsFollowedBySpace; + private final int[] mSortedSymbolsClusteringTogether; + private final int[] mSortedWordConnectors; + public final int[] mSortedWordSeparators; + public final PunctuationSuggestions mSuggestPuncList; + private final int mSentenceSeparator; + public final String mSentenceSeparatorAndSpace; + public final boolean mCurrentLanguageHasSpaces; + public final boolean mUsesAmericanTypography; + public final boolean mUsesGermanRules; + + public SpacingAndPunctuations(final Resources res) { + // To be able to binary search the code point. See {@link #isUsuallyPrecededBySpace(int)}. + mSortedSymbolsPrecededBySpace = StringUtils.toSortedCodePointArray( + res.getString(R.string.symbols_preceded_by_space)); + // To be able to binary search the code point. See {@link #isUsuallyFollowedBySpace(int)}. + mSortedSymbolsFollowedBySpace = StringUtils.toSortedCodePointArray( + res.getString(R.string.symbols_followed_by_space)); + mSortedSymbolsClusteringTogether = StringUtils.toSortedCodePointArray( + res.getString(R.string.symbols_clustering_together)); + // To be able to binary search the code point. See {@link #isWordConnector(int)}. + mSortedWordConnectors = StringUtils.toSortedCodePointArray( + res.getString(R.string.symbols_word_connectors)); + mSortedWordSeparators = StringUtils.toSortedCodePointArray( + res.getString(R.string.symbols_word_separators)); + mSentenceSeparator = res.getInteger(R.integer.sentence_separator); + mSentenceSeparatorAndSpace = new String(new int[] { + mSentenceSeparator, Constants.CODE_SPACE }, 0, 2); + mCurrentLanguageHasSpaces = res.getBoolean(R.bool.current_language_has_spaces); + final Locale locale = res.getConfiguration().locale; + // Heuristic: we use American Typography rules because it's the most common rules for all + // English variants. German rules (not "German typography") also have small gotchas. + mUsesAmericanTypography = Locale.ENGLISH.getLanguage().equals(locale.getLanguage()); + mUsesGermanRules = Locale.GERMAN.getLanguage().equals(locale.getLanguage()); + final String[] suggestPuncsSpec = MoreKeySpec.splitKeySpecs( + res.getString(R.string.suggested_punctuations)); + mSuggestPuncList = PunctuationSuggestions.newPunctuationSuggestions(suggestPuncsSpec); + } + + public boolean isWordSeparator(final int code) { + return Arrays.binarySearch(mSortedWordSeparators, code) >= 0; + } + + public boolean isWordConnector(final int code) { + return Arrays.binarySearch(mSortedWordConnectors, code) >= 0; + } + + public boolean isWordCodePoint(final int code) { + return Character.isLetter(code) || isWordConnector(code); + } + + public boolean isUsuallyPrecededBySpace(final int code) { + return Arrays.binarySearch(mSortedSymbolsPrecededBySpace, code) >= 0; + } + + public boolean isUsuallyFollowedBySpace(final int code) { + return Arrays.binarySearch(mSortedSymbolsFollowedBySpace, code) >= 0; + } + + public boolean isClusteringSymbol(final int code) { + return Arrays.binarySearch(mSortedSymbolsClusteringTogether, code) >= 0; + } + + public boolean isSentenceSeparator(final int code) { + return code == mSentenceSeparator; + } + + public String dump() { + final StringBuilder sb = new StringBuilder(); + sb.append("mSortedSymbolsPrecededBySpace = "); + sb.append("" + Arrays.toString(mSortedSymbolsPrecededBySpace)); + sb.append("\n mSortedSymbolsFollowedBySpace = "); + sb.append("" + Arrays.toString(mSortedSymbolsFollowedBySpace)); + sb.append("\n mSortedWordConnectors = "); + sb.append("" + Arrays.toString(mSortedWordConnectors)); + sb.append("\n mSortedWordSeparators = "); + sb.append("" + Arrays.toString(mSortedWordSeparators)); + sb.append("\n mSuggestPuncList = "); + sb.append("" + mSuggestPuncList); + sb.append("\n mSentenceSeparator = "); + sb.append("" + mSentenceSeparator); + sb.append("\n mSentenceSeparatorAndSpace = "); + sb.append("" + mSentenceSeparatorAndSpace); + sb.append("\n mCurrentLanguageHasSpaces = "); + sb.append("" + mCurrentLanguageHasSpaces); + sb.append("\n mUsesAmericanTypography = "); + sb.append("" + mUsesAmericanTypography); + sb.append("\n mUsesGermanRules = "); + sb.append("" + mUsesGermanRules); + return sb.toString(); + } +} diff --git a/java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java b/java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java index 050d8d26f..9585736e7 100644 --- a/java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java +++ b/java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java @@ -16,85 +16,51 @@ package com.android.inputmethod.latin.setup; -import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; -import android.os.Process; import android.preference.PreferenceManager; import android.util.Log; -import android.view.inputmethod.InputMethodManager; import com.android.inputmethod.compat.IntentCompatUtils; import com.android.inputmethod.latin.settings.Settings; /** - * This class detects the {@link Intent#ACTION_MY_PACKAGE_REPLACED} broadcast intent when this IME - * package has been replaced by a newer version of the same package. This class also detects + * This class handles the {@link Intent#ACTION_MY_PACKAGE_REPLACED} broadcast intent when this IME + * package has been replaced by a newer version of the same package. This class also handles * {@link Intent#ACTION_BOOT_COMPLETED} and {@link Intent#ACTION_USER_INITIALIZE} broadcast intent. * * If this IME has already been installed in the system image and a new version of this IME has - * been installed, {@link Intent#ACTION_MY_PACKAGE_REPLACED} is received by this receiver and it - * will hide the setup wizard's icon. + * been installed, {@link Intent#ACTION_MY_PACKAGE_REPLACED} is received to this class to hide the + * setup wizard's icon. * * If this IME has already been installed in the data partition and a new version of this IME has - * been installed, {@link Intent#ACTION_MY_PACKAGE_REPLACED} is received by this receiver but it + * been installed, {@link Intent#ACTION_MY_PACKAGE_REPLACED} is forwarded to this class but it * will not hide the setup wizard's icon, and the icon will appear on the launcher. * * If this IME hasn't been installed yet and has been newly installed, no * {@link Intent#ACTION_MY_PACKAGE_REPLACED} will be sent and the setup wizard's icon will appear * on the launcher. * - * When the device has been booted, {@link Intent#ACTION_BOOT_COMPLETED} is received by this - * receiver and it checks whether the setup wizard's icon should be appeared or not on the launcher + * When the device has been booted, {@link Intent#ACTION_BOOT_COMPLETED} is forwarded to this class + * to check whether the setup wizard's icon should be appeared or not on the launcher * depending on which partition this IME is installed. * - * When a multiuser account has been created, {@link Intent#ACTION_USER_INITIALIZE} is received - * by this receiver and it checks the whether the setup wizard's icon should be appeared or not on - * the launcher depending on which partition this IME is installed. + * When a multiuser account has been created, {@link Intent#ACTION_USER_INITIALIZE} is forwarded to + * this class to check whether the setup wizard's icon should be appeared or not on the launcher + * depending on which partition this IME is installed. */ -public final class LauncherIconVisibilityManager extends BroadcastReceiver { +public final class LauncherIconVisibilityManager { private static final String TAG = LauncherIconVisibilityManager.class.getSimpleName(); - @Override - public void onReceive(final Context context, final Intent intent) { - if (shouldHandleThisIntent(intent, context)) { + public static void onReceiveGlobalIntent(final String action, final Context context) { + if (Intent.ACTION_MY_PACKAGE_REPLACED.equals(action) || + Intent.ACTION_BOOT_COMPLETED.equals(action) || + IntentCompatUtils.is_ACTION_USER_INITIALIZE(action)) { updateSetupWizardIconVisibility(context); } - - // The process that hosts this broadcast receiver is invoked and remains alive even after - // 1) the package has been re-installed, 2) the device has just booted, - // 3) a new user has been created. - // There is no good reason to keep the process alive if this IME isn't a current IME. - final InputMethodManager imm = - (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE); - // Called to check whether this IME has been triggered by the current user or not - final boolean isInputMethodManagerValidForUserOfThisProcess = - !imm.getInputMethodList().isEmpty(); - final boolean isCurrentImeOfCurrentUser = isInputMethodManagerValidForUserOfThisProcess - && SetupActivity.isThisImeCurrent(context, imm); - if (!isCurrentImeOfCurrentUser) { - final int myPid = Process.myPid(); - Log.i(TAG, "Killing my process: pid=" + myPid); - Process.killProcess(myPid); - } - } - - private static boolean shouldHandleThisIntent(final Intent intent, final Context context) { - final String action = intent.getAction(); - if (Intent.ACTION_MY_PACKAGE_REPLACED.equals(action)) { - Log.i(TAG, "Package has been replaced: " + context.getPackageName()); - return true; - } else if (Intent.ACTION_BOOT_COMPLETED.equals(action)) { - Log.i(TAG, "Boot has been completed"); - return true; - } else if (IntentCompatUtils.is_ACTION_USER_INITIALIZE(action)) { - Log.i(TAG, "User initialize"); - return true; - } - return false; } public static void updateSetupWizardIconVisibility(final Context context) { diff --git a/java/src/com/android/inputmethod/latin/setup/SetupActivity.java b/java/src/com/android/inputmethod/latin/setup/SetupActivity.java index a68f98fe7..b770ea512 100644 --- a/java/src/com/android/inputmethod/latin/setup/SetupActivity.java +++ b/java/src/com/android/inputmethod/latin/setup/SetupActivity.java @@ -37,65 +37,4 @@ public final class SetupActivity extends Activity { finish(); } } - - /* - * We may not be able to get our own {@link InputMethodInfo} just after this IME is installed - * because {@link InputMethodManagerService} may not be aware of this IME yet. - * Note: {@link RichInputMethodManager} has similar methods. Here in setup wizard, we can't - * use it for the reason above. - */ - - /** - * Check if the IME specified by the context is enabled. - * CAVEAT: This may cause a round trip IPC. - * - * @param context package context of the IME to be checked. - * @param imm the {@link InputMethodManager}. - * @return true if this IME is enabled. - */ - /* package */ static boolean isThisImeEnabled(final Context context, - final InputMethodManager imm) { - final String packageName = context.getPackageName(); - for (final InputMethodInfo imi : imm.getEnabledInputMethodList()) { - if (packageName.equals(imi.getPackageName())) { - return true; - } - } - return false; - } - - /** - * Check if the IME specified by the context is the current IME. - * CAVEAT: This may cause a round trip IPC. - * - * @param context package context of the IME to be checked. - * @param imm the {@link InputMethodManager}. - * @return true if this IME is the current IME. - */ - /* package */ static boolean isThisImeCurrent(final Context context, - final InputMethodManager imm) { - final InputMethodInfo imi = getInputMethodInfoOf(context.getPackageName(), imm); - final String currentImeId = Settings.Secure.getString( - context.getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD); - return imi != null && imi.getId().equals(currentImeId); - } - - /** - * Get {@link InputMethodInfo} of the IME specified by the package name. - * CAVEAT: This may cause a round trip IPC. - * - * @param packageName package name of the IME. - * @param imm the {@link InputMethodManager}. - * @return the {@link InputMethodInfo} of the IME specified by the <code>packageName</code>, - * or null if not found. - */ - /* package */ static InputMethodInfo getInputMethodInfoOf(final String packageName, - final InputMethodManager imm) { - for (final InputMethodInfo imi : imm.getInputMethodList()) { - if (packageName.equals(imi.getPackageName())) { - return imi; - } - } - return null; - } } diff --git a/java/src/com/android/inputmethod/latin/setup/SetupStartIndicatorView.java b/java/src/com/android/inputmethod/latin/setup/SetupStartIndicatorView.java index 974dfddd3..73d25f6aa 100644 --- a/java/src/com/android/inputmethod/latin/setup/SetupStartIndicatorView.java +++ b/java/src/com/android/inputmethod/latin/setup/SetupStartIndicatorView.java @@ -21,13 +21,13 @@ import android.content.res.ColorStateList; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; +import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; -import com.android.inputmethod.compat.ViewCompatUtils; import com.android.inputmethod.latin.R; public final class SetupStartIndicatorView extends LinearLayout { @@ -96,13 +96,13 @@ public final class SetupStartIndicatorView extends LinearLayout { @Override protected void onDraw(final Canvas canvas) { super.onDraw(canvas); - final int layoutDirection = ViewCompatUtils.getLayoutDirection(this); + final int layoutDirection = ViewCompat.getLayoutDirection(this); final int width = getWidth(); final int height = getHeight(); final float halfHeight = height / 2.0f; final Path path = mIndicatorPath; path.rewind(); - if (layoutDirection == ViewCompatUtils.LAYOUT_DIRECTION_RTL) { + if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL) { // Left arrow path.moveTo(width, 0.0f); path.lineTo(0.0f, halfHeight); diff --git a/java/src/com/android/inputmethod/latin/setup/SetupStepIndicatorView.java b/java/src/com/android/inputmethod/latin/setup/SetupStepIndicatorView.java index c909507c6..6734e61b8 100644 --- a/java/src/com/android/inputmethod/latin/setup/SetupStepIndicatorView.java +++ b/java/src/com/android/inputmethod/latin/setup/SetupStepIndicatorView.java @@ -20,10 +20,10 @@ import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; +import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.view.View; -import com.android.inputmethod.compat.ViewCompatUtils; import com.android.inputmethod.latin.R; public final class SetupStepIndicatorView extends View { @@ -38,12 +38,12 @@ public final class SetupStepIndicatorView extends View { } public void setIndicatorPosition(final int stepPos, final int totalStepNum) { - final int layoutDirection = ViewCompatUtils.getLayoutDirection(this); + final int layoutDirection = ViewCompat.getLayoutDirection(this); // The indicator position is the center of the partition that is equally divided into // the total step number. final float partionWidth = 1.0f / totalStepNum; final float pos = stepPos * partionWidth + partionWidth / 2.0f; - mXRatio = (layoutDirection == ViewCompatUtils.LAYOUT_DIRECTION_RTL) ? 1.0f - pos : pos; + mXRatio = (layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL) ? 1.0f - pos : pos; invalidate(); } diff --git a/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java b/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java index c4a813c24..e455e53d3 100644 --- a/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java +++ b/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java @@ -37,8 +37,8 @@ import com.android.inputmethod.compat.TextViewCompatUtils; import com.android.inputmethod.compat.ViewCompatUtils; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.settings.SettingsActivity; -import com.android.inputmethod.latin.utils.CollectionUtils; -import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper; +import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper; +import com.android.inputmethod.latin.utils.UncachedInputMethodManagerUtils; import java.util.ArrayList; @@ -74,27 +74,28 @@ public final class SetupWizardActivity extends Activity implements View.OnClickL private SettingsPoolingHandler mHandler; private static final class SettingsPoolingHandler - extends StaticInnerHandlerWrapper<SetupWizardActivity> { + extends LeakGuardHandlerWrapper<SetupWizardActivity> { private static final int MSG_POLLING_IME_SETTINGS = 0; private static final long IME_SETTINGS_POLLING_INTERVAL = 200; private final InputMethodManager mImmInHandler; - public SettingsPoolingHandler(final SetupWizardActivity outerInstance, + public SettingsPoolingHandler(final SetupWizardActivity ownerInstance, final InputMethodManager imm) { - super(outerInstance); + super(ownerInstance); mImmInHandler = imm; } @Override public void handleMessage(final Message msg) { - final SetupWizardActivity setupWizardActivity = getOuterInstance(); + final SetupWizardActivity setupWizardActivity = getOwnerInstance(); if (setupWizardActivity == null) { return; } switch (msg.what) { case MSG_POLLING_IME_SETTINGS: - if (SetupActivity.isThisImeEnabled(setupWizardActivity, mImmInHandler)) { + if (UncachedInputMethodManagerUtils.isThisImeEnabled(setupWizardActivity, + mImmInHandler)) { setupWizardActivity.invokeSetupWizardOfThisIme(); return; } @@ -278,7 +279,8 @@ public final class SetupWizardActivity extends Activity implements View.OnClickL } void invokeSubtypeEnablerOfThisIme() { - final InputMethodInfo imi = SetupActivity.getInputMethodInfoOf(getPackageName(), mImm); + final InputMethodInfo imi = + UncachedInputMethodManagerUtils.getInputMethodInfoOf(getPackageName(), mImm); if (imi == null) { return; } @@ -302,10 +304,10 @@ public final class SetupWizardActivity extends Activity implements View.OnClickL private int determineSetupStepNumber() { mHandler.cancelPollingImeSettings(); - if (!SetupActivity.isThisImeEnabled(this, mImm)) { + if (!UncachedInputMethodManagerUtils.isThisImeEnabled(this, mImm)) { return STEP_1; } - if (!SetupActivity.isThisImeCurrent(this, mImm)) { + if (!UncachedInputMethodManagerUtils.isThisImeCurrent(this, mImm)) { return STEP_2; } return STEP_3; @@ -482,7 +484,7 @@ public final class SetupWizardActivity extends Activity implements View.OnClickL static final class SetupStepGroup { private final SetupStepIndicatorView mIndicatorView; - private final ArrayList<SetupStep> mGroup = CollectionUtils.newArrayList(); + private final ArrayList<SetupStep> mGroup = new ArrayList<>(); public SetupStepGroup(final SetupStepIndicatorView indicatorView) { mIndicatorView = indicatorView; diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java index 503b18b1b..90c8f618f 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java @@ -27,18 +27,17 @@ import android.view.inputmethod.InputMethodSubtype; import android.view.textservice.SuggestionsInfo; import com.android.inputmethod.keyboard.KeyboardLayoutSet; -import com.android.inputmethod.latin.BinaryDictionary; import com.android.inputmethod.latin.ContactsBinaryDictionary; import com.android.inputmethod.latin.Dictionary; import com.android.inputmethod.latin.DictionaryCollection; import com.android.inputmethod.latin.DictionaryFactory; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.SynchronouslyLoadedContactsBinaryDictionary; -import com.android.inputmethod.latin.SynchronouslyLoadedUserBinaryDictionary; import com.android.inputmethod.latin.UserBinaryDictionary; import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils; +import com.android.inputmethod.latin.utils.BinaryDictionaryUtils; import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.LocaleUtils; +import com.android.inputmethod.latin.utils.ScriptUtils; import com.android.inputmethod.latin.utils.StringUtils; import java.lang.ref.WeakReference; @@ -49,7 +48,6 @@ import java.util.HashSet; import java.util.Iterator; import java.util.Locale; import java.util.Map; -import java.util.TreeMap; /** * Service for spell checking, using LatinIME's dictionaries and mechanisms. @@ -78,42 +76,10 @@ public final class AndroidSpellCheckerService extends SpellCheckerService private final Object mUseContactsLock = new Object(); private final HashSet<WeakReference<DictionaryCollection>> mDictionaryCollectionsList = - CollectionUtils.newHashSet(); + new HashSet<>(); - public static final int SCRIPT_LATIN = 0; - public static final int SCRIPT_CYRILLIC = 1; - public static final int SCRIPT_GREEK = 2; 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 - // words written in another script than the selected script, because we know we - // don't have those in our dictionary so we will underline everything and we - // will never have any suggestions, so it makes no sense checking them, and this - // is done in {@link #shouldFilterOut}. Also, the script is used to choose which - // 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 = CollectionUtils.newTreeMap(); - mLanguageToScript.put("cs", SCRIPT_LATIN); - mLanguageToScript.put("da", SCRIPT_LATIN); - mLanguageToScript.put("de", SCRIPT_LATIN); - mLanguageToScript.put("el", SCRIPT_GREEK); - mLanguageToScript.put("en", SCRIPT_LATIN); - mLanguageToScript.put("es", SCRIPT_LATIN); - mLanguageToScript.put("fi", SCRIPT_LATIN); - mLanguageToScript.put("fr", SCRIPT_LATIN); - mLanguageToScript.put("hr", SCRIPT_LATIN); - mLanguageToScript.put("it", SCRIPT_LATIN); - mLanguageToScript.put("lt", SCRIPT_LATIN); - mLanguageToScript.put("lv", SCRIPT_LATIN); - mLanguageToScript.put("nb", SCRIPT_LATIN); - mLanguageToScript.put("nl", SCRIPT_LATIN); - mLanguageToScript.put("pt", SCRIPT_LATIN); - mLanguageToScript.put("sl", SCRIPT_LATIN); - mLanguageToScript.put("ru", SCRIPT_CYRILLIC); - } @Override public void onCreate() { super.onCreate(); @@ -124,22 +90,13 @@ public final class AndroidSpellCheckerService extends SpellCheckerService onSharedPreferenceChanged(prefs, PREF_USE_CONTACTS_KEY); } - 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: \"" - + locale.getLanguage() + "\". Framework bug?"); - } - return script; - } - private static String getKeyboardLayoutNameForScript(final int script) { switch (script) { - case AndroidSpellCheckerService.SCRIPT_LATIN: + case ScriptUtils.SCRIPT_LATIN: return "qwerty"; - case AndroidSpellCheckerService.SCRIPT_CYRILLIC: + case ScriptUtils.SCRIPT_CYRILLIC: return "east_slavic"; - case AndroidSpellCheckerService.SCRIPT_GREEK: + case ScriptUtils.SCRIPT_GREEK: return "greek"; default: throw new RuntimeException("Wrong script supplied: " + script); @@ -256,7 +213,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService mOriginalText = originalText; mRecommendedThreshold = recommendedThreshold; mMaxLength = maxLength; - mSuggestions = CollectionUtils.newArrayList(maxLength + 1); + mSuggestions = new ArrayList<>(maxLength + 1); mScores = new int[mMaxLength]; } @@ -267,6 +224,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService // if it doesn't. See documentation for binarySearch. final int insertIndex = positionIndex >= 0 ? positionIndex : -positionIndex - 1; + // Weak <- insertIndex == 0, ..., insertIndex == mLength -> Strong if (insertIndex == 0 && mLength >= mMaxLength) { // In the future, we may want to keep track of the best suggestion score even if // we are asked for 0 suggestions. In this case, we can use the following @@ -284,11 +242,6 @@ public final class AndroidSpellCheckerService extends SpellCheckerService // } return true; } - if (insertIndex >= mMaxLength) { - // We found a suggestion, but its score is too weak to be kept considering - // the suggestion limit. - return true; - } final String wordString = new String(word, wordOffset, wordLength); if (mLength < mMaxLength) { @@ -296,12 +249,13 @@ public final class AndroidSpellCheckerService extends SpellCheckerService ++mLength; System.arraycopy(mScores, insertIndex, mScores, insertIndex + 1, copyLen); mSuggestions.add(insertIndex, wordString); + mScores[insertIndex] = score; } else { - System.arraycopy(mScores, 1, mScores, 0, insertIndex); + System.arraycopy(mScores, 1, mScores, 0, insertIndex - 1); mSuggestions.add(insertIndex, wordString); mSuggestions.remove(0); + mScores[insertIndex - 1] = score; } - mScores[insertIndex] = score; return true; } @@ -320,7 +274,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService hasRecommendedSuggestions = false; } else { gatheredSuggestions = EMPTY_STRING_ARRAY; - final float normalizedScore = BinaryDictionary.calcNormalizedScore( + final float normalizedScore = BinaryDictionaryUtils.calcNormalizedScore( mOriginalText, mBestSuggestion, mBestScore); hasRecommendedSuggestions = (normalizedScore > mRecommendedThreshold); } @@ -355,7 +309,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService final int bestScore = mScores[mLength - 1]; final String bestSuggestion = mSuggestions.get(0); final float normalizedScore = - BinaryDictionary.calcNormalizedScore( + BinaryDictionaryUtils.calcNormalizedScore( mOriginalText, bestSuggestion.toString(), bestScore); hasRecommendedSuggestions = (normalizedScore > mRecommendedThreshold); if (DBG) { @@ -383,6 +337,8 @@ public final class AndroidSpellCheckerService extends SpellCheckerService new Thread("spellchecker_close_dicts") { @Override public void run() { + // Contacts dictionary can be closed multiple times here. If the dictionary is + // already closed, extra closings are no-ops, so it's safe. for (DictionaryPool pool : oldPools.values()) { pool.close(); } @@ -416,10 +372,10 @@ public final class AndroidSpellCheckerService extends SpellCheckerService } public DictAndKeyboard createDictAndKeyboard(final Locale locale) { - final int script = getScriptFromLocale(locale); + final int script = ScriptUtils.getScriptFromSpellCheckerLocale(locale); final String keyboardLayoutName = getKeyboardLayoutNameForScript(script); - final InputMethodSubtype subtype = AdditionalSubtypeUtils.createAdditionalSubtype( - locale.toString(), keyboardLayoutName, null); + final InputMethodSubtype subtype = AdditionalSubtypeUtils.createDummyAdditionalSubtype( + locale.toString(), keyboardLayoutName); final KeyboardLayoutSet keyboardLayoutSet = createKeyboardSetForSpellChecker(subtype); final DictionaryCollection dictionaryCollection = @@ -428,7 +384,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService final String localeStr = locale.toString(); UserBinaryDictionary userDictionary = mUserDictionaries.get(localeStr); if (null == userDictionary) { - userDictionary = new SynchronouslyLoadedUserBinaryDictionary(this, localeStr, true); + userDictionary = new SynchronouslyLoadedUserBinaryDictionary(this, locale, true); mUserDictionaries.put(localeStr, userDictionary); } dictionaryCollection.addDictionary(userDictionary); @@ -443,8 +399,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService } } dictionaryCollection.addDictionary(mContactsDictionary); - mDictionaryCollectionsList.add( - new WeakReference<DictionaryCollection>(dictionaryCollection)); + mDictionaryCollectionsList.add(new WeakReference<>(dictionaryCollection)); } return new DictAndKeyboard(dictionaryCollection, keyboardLayoutSet); } diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java index ddda52d71..6bfd354ea 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java @@ -16,6 +16,7 @@ package com.android.inputmethod.latin.spellcheck; +import android.content.res.Resources; import android.os.Binder; import android.text.TextUtils; import android.util.Log; @@ -23,17 +24,21 @@ import android.view.textservice.SentenceSuggestionsInfo; import android.view.textservice.SuggestionsInfo; import android.view.textservice.TextInfo; -import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.latin.PrevWordsInfo; import java.util.ArrayList; +import java.util.Locale; 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]; + private final Resources mResources; + private SentenceLevelAdapter mSentenceLevelAdapter; public AndroidSpellCheckerSession(AndroidSpellCheckerService service) { super(service); + mResources = service.getResources(); } private SentenceSuggestionsInfo fixWronglyInvalidatedWordWithSingleQuote(TextInfo ti, @@ -43,10 +48,9 @@ public final class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheck 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(); + final ArrayList<Integer> additionalOffsets = new ArrayList<>(); + final ArrayList<Integer> additionalLengths = new ArrayList<>(); + final ArrayList<SuggestionsInfo> additionalSuggestionsInfos = new ArrayList<>(); String currentWord = null; for (int i = 0; i < N; ++i) { final SuggestionsInfo si = ssi.getSuggestionsInfoAt(i); @@ -57,7 +61,8 @@ public final class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheck final int offset = ssi.getOffsetAt(i); final int length = ssi.getLengthAt(i); final String subText = typedText.substring(offset, offset + length); - final String prevWord = currentWord; + final PrevWordsInfo prevWordsInfo = + new PrevWordsInfo(new PrevWordsInfo.WordInfo(currentWord)); currentWord = subText; if (!subText.contains(AndroidSpellCheckerService.SINGLE_QUOTE)) { continue; @@ -73,7 +78,7 @@ public final class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheck if (TextUtils.isEmpty(splitText)) { continue; } - if (mSuggestionsCache.getSuggestionsFromCache(splitText, prevWord) == null) { + if (mSuggestionsCache.getSuggestionsFromCache(splitText, prevWordsInfo) == null) { continue; } final int newLength = splitText.length(); @@ -116,8 +121,7 @@ public final class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheck @Override public SentenceSuggestionsInfo[] onGetSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit) { - final SentenceSuggestionsInfo[] retval = - super.onGetSentenceSuggestionsMultiple(textInfos, suggestionsLimit); + final SentenceSuggestionsInfo[] retval = splitAndSuggest(textInfos, suggestionsLimit); if (retval == null || retval.length != textInfos.length) { return retval; } @@ -131,6 +135,58 @@ public final class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheck return retval; } + /** + * Get sentence suggestions for specified texts in an array of TextInfo. This is taken from + * SpellCheckerService#onGetSentenceSuggestionsMultiple that we can't use because it's + * using private variables. + * The default implementation splits the input text to words and returns + * {@link SentenceSuggestionsInfo} which contains suggestions for each word. + * This function will run on the incoming IPC thread. + * So, this is not called on the main thread, + * but will be called in series on another thread. + * @param textInfos an array of the text metadata + * @param suggestionsLimit the maximum number of suggestions to be returned + * @return an array of {@link SentenceSuggestionsInfo} returned by + * {@link SpellCheckerService.Session#onGetSuggestions(TextInfo, int)} + */ + private SentenceSuggestionsInfo[] splitAndSuggest(TextInfo[] textInfos, int suggestionsLimit) { + if (textInfos == null || textInfos.length == 0) { + return SentenceLevelAdapter.EMPTY_SENTENCE_SUGGESTIONS_INFOS; + } + SentenceLevelAdapter sentenceLevelAdapter; + synchronized(this) { + sentenceLevelAdapter = mSentenceLevelAdapter; + if (sentenceLevelAdapter == null) { + final String localeStr = getLocale(); + if (!TextUtils.isEmpty(localeStr)) { + sentenceLevelAdapter = new SentenceLevelAdapter(mResources, + new Locale(localeStr)); + mSentenceLevelAdapter = sentenceLevelAdapter; + } + } + } + if (sentenceLevelAdapter == null) { + return SentenceLevelAdapter.EMPTY_SENTENCE_SUGGESTIONS_INFOS; + } + final int infosSize = textInfos.length; + final SentenceSuggestionsInfo[] retval = new SentenceSuggestionsInfo[infosSize]; + for (int i = 0; i < infosSize; ++i) { + final SentenceLevelAdapter.SentenceTextInfoParams textInfoParams = + sentenceLevelAdapter.getSplitWords(textInfos[i]); + final ArrayList<SentenceLevelAdapter.SentenceWordItem> mItems = + textInfoParams.mItems; + final int itemsSize = mItems.size(); + final TextInfo[] splitTextInfos = new TextInfo[itemsSize]; + for (int j = 0; j < itemsSize; ++j) { + splitTextInfos[j] = mItems.get(j).mTextInfo; + } + retval[i] = SentenceLevelAdapter.reconstructSuggestions( + textInfoParams, onGetSuggestionsMultiple( + splitTextInfos, suggestionsLimit, true)); + } + return retval; + } + @Override public SuggestionsInfo[] onGetSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) { @@ -148,7 +204,9 @@ public final class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheck } else { prevWord = null; } - retval[i] = onGetSuggestionsInternal(textInfos[i], prevWord, suggestionsLimit); + final PrevWordsInfo prevWordsInfo = + new PrevWordsInfo(new PrevWordsInfo.WordInfo(prevWord)); + retval[i] = onGetSuggestionsInternal(textInfos[i], prevWordsInfo, suggestionsLimit); retval[i].setCookieAndSequence(textInfos[i].getCookie(), textInfos[i].getSequence()); } diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java index d6e5b75ad..4825b9e2c 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java @@ -30,10 +30,13 @@ import android.view.textservice.TextInfo; import com.android.inputmethod.compat.SuggestionsInfoCompatUtils; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.Dictionary; +import com.android.inputmethod.latin.PrevWordsInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.WordComposer; import com.android.inputmethod.latin.spellcheck.AndroidSpellCheckerService.SuggestionsGatherer; +import com.android.inputmethod.latin.utils.CoordinateUtils; import com.android.inputmethod.latin.utils.LocaleUtils; +import com.android.inputmethod.latin.utils.ScriptUtils; import com.android.inputmethod.latin.utils.StringUtils; import java.util.ArrayList; @@ -66,29 +69,29 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { 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); + new LruCache<>(MAX_CACHE_SIZE); // TODO: Support n-gram input - private static String generateKey(String query, String prevWord) { - if (TextUtils.isEmpty(query) || TextUtils.isEmpty(prevWord)) { + private static String generateKey(final String query, final PrevWordsInfo prevWordsInfo) { + if (TextUtils.isEmpty(query) || !prevWordsInfo.isValid()) { return query; } - return query + CHAR_DELIMITER + prevWord; + return query + CHAR_DELIMITER + prevWordsInfo; } - // TODO: Support n-gram input - public SuggestionsParams getSuggestionsFromCache(String query, String prevWord) { - return mUnigramSuggestionsInfoCache.get(generateKey(query, prevWord)); + public SuggestionsParams getSuggestionsFromCache(String query, + final PrevWordsInfo prevWordsInfo) { + return mUnigramSuggestionsInfoCache.get(generateKey(query, prevWordsInfo)); } - // TODO: Support n-gram input public void putSuggestionsToCache( - String query, String prevWord, String[] suggestions, int flags) { + final String query, final PrevWordsInfo prevWordsInfo, + final String[] suggestions, final int flags) { if (suggestions == null || TextUtils.isEmpty(query)) { return; } mUnigramSuggestionsInfoCache.put( - generateKey(query, prevWord), new SuggestionsParams(suggestions, flags)); + generateKey(query, prevWordsInfo), new SuggestionsParams(suggestions, flags)); } public void clearCache() { @@ -114,7 +117,7 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { final String localeString = getLocale(); mDictionaryPool = mService.getDictionaryPool(localeString); mLocale = LocaleUtils.constructLocaleFromString(localeString); - mScript = AndroidSpellCheckerService.getScriptFromLocale(mLocale); + mScript = ScriptUtils.getScriptFromSpellCheckerLocale(mLocale); } @Override @@ -123,44 +126,6 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { 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); - case AndroidSpellCheckerService.SCRIPT_GREEK: - // Greek letters are either in the 370~3FF range (Greek & Coptic), or in the - // 1F00~1FFF range (Greek extended). Our dictionary contains both sort of characters. - // Our dictionary also contains a few words with 0xF2; it would be best to check - // if that's correct, but a web search does return results for these words so - // they are probably okay. - return (codePoint >= 0x370 && codePoint <= 0x3FF) - || (codePoint >= 0x1F00 && codePoint <= 0x1FFF) - || codePoint == 0xF2; - default: - // Should never come here - throw new RuntimeException("Impossible value of script: " + script); - } - } - private static final int CHECKABILITY_CHECKABLE = 0; private static final int CHECKABILITY_TOO_MANY_NON_LETTERS = 1; private static final int CHECKABILITY_CONTAINS_PERIOD = 2; @@ -187,7 +152,7 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { // 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) + if (!ScriptUtils.isLetterPartOfScript(firstCodePoint, script) && '\'' != firstCodePoint) return CHECKABILITY_FIRST_LETTER_UNCHECKABLE; // Filter contents @@ -208,7 +173,7 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { if (Constants.CODE_PERIOD == codePoint) { return CHECKABILITY_CONTAINS_PERIOD; } - if (isLetterCheckableByLanguage(codePoint, script)) ++letterCount; + if (ScriptUtils.isLetterPartOfScript(codePoint, script)) ++letterCount; } // Guestimate heuristic: perform spell checking if at least 3/4 of the characters // in this word are letters @@ -257,11 +222,12 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { } protected SuggestionsInfo onGetSuggestionsInternal( - final TextInfo textInfo, final String prevWord, final int suggestionsLimit) { + final TextInfo textInfo, final PrevWordsInfo prevWordsInfo, + final int suggestionsLimit) { try { final String inText = textInfo.getText(); final SuggestionsParams cachedSuggestionsParams = - mSuggestionsCache.getSuggestionsFromCache(inText, prevWord); + mSuggestionsCache.getSuggestionsFromCache(inText, prevWordsInfo); if (cachedSuggestionsParams != null) { if (DBG) { Log.d(TAG, "Cache hit: " + inText + ", " + cachedSuggestionsParams.mFlags); @@ -279,6 +245,24 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { return AndroidSpellCheckerService.getNotInDictEmptySuggestions( false /* reportAsTypo */); } + if (CHECKABILITY_CONTAINS_PERIOD == checkability) { + final String[] splitText = inText.split(Constants.REGEXP_PERIOD); + boolean allWordsAreValid = true; + for (final String word : splitText) { + if (!dictInfo.mDictionary.isValidWord(word)) { + allWordsAreValid = false; + break; + } + } + if (allWordsAreValid) { + return new SuggestionsInfo(SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO + | SuggestionsInfo.RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS, + new String[] { + TextUtils.join(Constants.STRING_SPACE, splitText), + TextUtils.join(Constants.STRING_PERIOD_AND_SPACE, + splitText) }); + } + } return dictInfo.mDictionary.isValidWord(inText) ? AndroidSpellCheckerService.getInDictEmptySuggestions() : AndroidSpellCheckerService.getNotInDictEmptySuggestions( @@ -312,16 +296,21 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { false /* reportAsTypo */); } final WordComposer composer = new WordComposer(); - final int length = text.length(); - for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) { - final int codePoint = text.codePointAt(i); - composer.addKeyInfo(codePoint, dictInfo.getKeyboard(codePoint)); + final int[] codePoints = StringUtils.toCodePointArray(text); + final int[] coordinates; + if (null == dictInfo.mKeyboard) { + coordinates = CoordinateUtils.newCoordinateArray(codePoints.length, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); + } else { + coordinates = dictInfo.mKeyboard.getCoordinates(codePoints); } + composer.setComposingWord(codePoints, coordinates); // TODO: make a spell checker option to block offensive words or not final ArrayList<SuggestedWordInfo> suggestions = - dictInfo.mDictionary.getSuggestions(composer, prevWord, + dictInfo.mDictionary.getSuggestions(composer, prevWordsInfo, dictInfo.getProximityInfo(), true /* blockOffensiveWords */, - null /* additionalFeaturesOptions */); + null /* additionalFeaturesOptions */, 0 /* sessionId */, + null /* inOutLanguageWeight */); if (suggestions != null) { for (final SuggestedWordInfo suggestion : suggestions) { final String suggestionStr = suggestion.mWord; @@ -362,7 +351,8 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { .getValueOf_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS() : 0); final SuggestionsInfo retval = new SuggestionsInfo(flags, result.mSuggestions); - mSuggestionsCache.putSuggestionsToCache(text, prevWord, result.mSuggestions, flags); + mSuggestionsCache.putSuggestionsToCache(text, prevWordsInfo, result.mSuggestions, + flags); return retval; } catch (RuntimeException e) { // Don't kill the keyboard if there is a bug in the spell checker diff --git a/java/src/com/android/inputmethod/latin/spellcheck/DictAndKeyboard.java b/java/src/com/android/inputmethod/latin/spellcheck/DictAndKeyboard.java index b77f3e2c5..b33739fc1 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/DictAndKeyboard.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/DictAndKeyboard.java @@ -16,38 +16,27 @@ package com.android.inputmethod.latin.spellcheck; -import com.android.inputmethod.latin.Dictionary; import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardId; import com.android.inputmethod.keyboard.KeyboardLayoutSet; import com.android.inputmethod.keyboard.ProximityInfo; +import com.android.inputmethod.latin.Dictionary; /** * A container for a Dictionary and a Keyboard. */ public final class DictAndKeyboard { public final Dictionary mDictionary; - private final Keyboard mKeyboard; - private final Keyboard mManualShiftedKeyboard; + public final Keyboard mKeyboard; public DictAndKeyboard( final Dictionary dictionary, final KeyboardLayoutSet keyboardLayoutSet) { mDictionary = dictionary; if (keyboardLayoutSet == null) { mKeyboard = null; - mManualShiftedKeyboard = null; return; } mKeyboard = keyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET); - mManualShiftedKeyboard = - keyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED); - } - - public Keyboard getKeyboard(final int codePoint) { - if (mKeyboard == null) { - return null; - } - return mKeyboard.getKey(codePoint) != null ? mKeyboard : mManualShiftedKeyboard; } public ProximityInfo getProximityInfo() { diff --git a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java index a0aed2829..cc52a3e0f 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java @@ -20,9 +20,9 @@ import android.util.Log; import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.Dictionary; +import com.android.inputmethod.latin.PrevWordsInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.WordComposer; -import com.android.inputmethod.latin.utils.CollectionUtils; import java.util.ArrayList; import java.util.Locale; @@ -46,17 +46,19 @@ public final class DictionaryPool extends LinkedBlockingQueue<DictAndKeyboard> { private final Locale mLocale; private int mSize; private volatile boolean mClosed; - final static ArrayList<SuggestedWordInfo> noSuggestions = CollectionUtils.newArrayList(); + final static ArrayList<SuggestedWordInfo> noSuggestions = new ArrayList<>(); private final static DictAndKeyboard dummyDict = new DictAndKeyboard( new Dictionary(Dictionary.TYPE_MAIN) { + // TODO: this dummy dictionary should be a singleton in the Dictionary class. @Override public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, - final String prevWord, final ProximityInfo proximityInfo, - final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) { + final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, + final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, + final int sessionId, final float[] inOutLanguageWeight) { return noSuggestions; } @Override - public boolean isValidWord(final String word) { + public boolean isInDictionary(final String 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). diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SentenceLevelAdapter.java b/java/src/com/android/inputmethod/latin/spellcheck/SentenceLevelAdapter.java new file mode 100644 index 000000000..13352f39e --- /dev/null +++ b/java/src/com/android/inputmethod/latin/spellcheck/SentenceLevelAdapter.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.spellcheck; + +import android.content.res.Resources; +import android.view.textservice.SentenceSuggestionsInfo; +import android.view.textservice.SuggestionsInfo; +import android.view.textservice.TextInfo; + +import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.settings.SpacingAndPunctuations; +import com.android.inputmethod.latin.utils.RunInLocale; + +import java.util.ArrayList; +import java.util.Locale; + +/** + * This code is mostly lifted directly from android.service.textservice.SpellCheckerService in + * the framework; maybe that should be protected instead, so that implementers don't have to + * rewrite everything for any small change. + */ +public class SentenceLevelAdapter { + public static final SentenceSuggestionsInfo[] EMPTY_SENTENCE_SUGGESTIONS_INFOS = + new SentenceSuggestionsInfo[] {}; + private static final SuggestionsInfo EMPTY_SUGGESTIONS_INFO = new SuggestionsInfo(0, null); + /** + * Container for split TextInfo parameters + */ + public static class SentenceWordItem { + public final TextInfo mTextInfo; + public final int mStart; + public final int mLength; + public SentenceWordItem(TextInfo ti, int start, int end) { + mTextInfo = ti; + mStart = start; + mLength = end - start; + } + } + + /** + * Container for originally queried TextInfo and parameters + */ + public static class SentenceTextInfoParams { + final TextInfo mOriginalTextInfo; + final ArrayList<SentenceWordItem> mItems; + final int mSize; + public SentenceTextInfoParams(TextInfo ti, ArrayList<SentenceWordItem> items) { + mOriginalTextInfo = ti; + mItems = items; + mSize = items.size(); + } + } + + private static class WordIterator { + private final SpacingAndPunctuations mSpacingAndPunctuations; + public WordIterator(final Resources res, final Locale locale) { + final RunInLocale<SpacingAndPunctuations> job + = new RunInLocale<SpacingAndPunctuations>() { + @Override + protected SpacingAndPunctuations job(final Resources res) { + return new SpacingAndPunctuations(res); + } + }; + mSpacingAndPunctuations = job.runInLocale(res, locale); + } + + public int getEndOfWord(final CharSequence sequence, int index) { + final int length = sequence.length(); + index = index < 0 ? 0 : Character.offsetByCodePoints(sequence, index, 1); + while (index < length) { + final int codePoint = Character.codePointAt(sequence, index); + if (mSpacingAndPunctuations.isWordSeparator(codePoint)) { + // If it's a period, we want to stop here only if it's followed by another + // word separator. In all other cases we stop here. + if (Constants.CODE_PERIOD == codePoint) { + final int indexOfNextCodePoint = + index + Character.charCount(Constants.CODE_PERIOD); + if (indexOfNextCodePoint < length + && mSpacingAndPunctuations.isWordSeparator( + Character.codePointAt(sequence, indexOfNextCodePoint))) { + return index; + } + } else { + return index; + } + } + index += Character.charCount(codePoint); + } + return index; + } + + public int getBeginningOfNextWord(final CharSequence sequence, int index) { + final int length = sequence.length(); + if (index >= length) { + return -1; + } + index = index < 0 ? 0 : Character.offsetByCodePoints(sequence, index, 1); + while (index < length) { + final int codePoint = Character.codePointAt(sequence, index); + if (!mSpacingAndPunctuations.isWordSeparator(codePoint)) { + return index; + } + index += Character.charCount(codePoint); + } + return -1; + } + } + + private final WordIterator mWordIterator; + public SentenceLevelAdapter(final Resources res, final Locale locale) { + mWordIterator = new WordIterator(res, locale); + } + + public SentenceTextInfoParams getSplitWords(TextInfo originalTextInfo) { + final WordIterator wordIterator = mWordIterator; + final CharSequence originalText = originalTextInfo.getText(); + final int cookie = originalTextInfo.getCookie(); + final int start = -1; + final int end = originalText.length(); + final ArrayList<SentenceWordItem> wordItems = new ArrayList<SentenceWordItem>(); + int wordStart = wordIterator.getBeginningOfNextWord(originalText, start); + int wordEnd = wordIterator.getEndOfWord(originalText, wordStart); + while (wordStart <= end && wordEnd != -1 && wordStart != -1) { + if (wordEnd >= start && wordEnd > wordStart) { + final String query = originalText.subSequence(wordStart, wordEnd).toString(); + final TextInfo ti = new TextInfo(query, cookie, query.hashCode()); + wordItems.add(new SentenceWordItem(ti, wordStart, wordEnd)); + } + wordStart = wordIterator.getBeginningOfNextWord(originalText, wordEnd); + if (wordStart == -1) { + break; + } + wordEnd = wordIterator.getEndOfWord(originalText, wordStart); + } + return new SentenceTextInfoParams(originalTextInfo, wordItems); + } + + public static SentenceSuggestionsInfo reconstructSuggestions( + SentenceTextInfoParams originalTextInfoParams, SuggestionsInfo[] results) { + if (results == null || results.length == 0) { + return null; + } + if (originalTextInfoParams == null) { + return null; + } + final int originalCookie = originalTextInfoParams.mOriginalTextInfo.getCookie(); + final int originalSequence = + originalTextInfoParams.mOriginalTextInfo.getSequence(); + + final int querySize = originalTextInfoParams.mSize; + final int[] offsets = new int[querySize]; + final int[] lengths = new int[querySize]; + final SuggestionsInfo[] reconstructedSuggestions = new SuggestionsInfo[querySize]; + for (int i = 0; i < querySize; ++i) { + final SentenceWordItem item = originalTextInfoParams.mItems.get(i); + SuggestionsInfo result = null; + for (int j = 0; j < results.length; ++j) { + final SuggestionsInfo cur = results[j]; + if (cur != null && cur.getSequence() == item.mTextInfo.getSequence()) { + result = cur; + result.setCookieAndSequence(originalCookie, originalSequence); + break; + } + } + offsets[i] = item.mStart; + lengths[i] = item.mLength; + reconstructedSuggestions[i] = result != null ? result : EMPTY_SUGGESTIONS_INFO; + } + return new SentenceSuggestionsInfo(reconstructedSuggestions, offsets, lengths); + } +} diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java index 999ca775b..186dafd29 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java @@ -39,7 +39,7 @@ public final class SpellCheckerSettingsFragment extends PreferenceFragment { addPreferencesFromResource(R.xml.spell_checker_settings); final PreferenceScreen preferenceScreen = getPreferenceScreen(); if (preferenceScreen != null) { - preferenceScreen.setTitle(ApplicationUtils.getAcitivityTitleResId( + preferenceScreen.setTitle(ApplicationUtils.getActivityTitleResId( getActivity(), SpellCheckerSettingsActivity.class)); } } diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/spellcheck/SynchronouslyLoadedContactsBinaryDictionary.java index 3213c92c7..a6437bac3 100644 --- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/SynchronouslyLoadedContactsBinaryDictionary.java @@ -14,45 +14,42 @@ * limitations under the License. */ -package com.android.inputmethod.latin; +package com.android.inputmethod.latin.spellcheck; import android.content.Context; import com.android.inputmethod.keyboard.ProximityInfo; +import com.android.inputmethod.latin.ContactsBinaryDictionary; +import com.android.inputmethod.latin.PrevWordsInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import com.android.inputmethod.latin.WordComposer; import java.util.ArrayList; import java.util.Locale; public final class SynchronouslyLoadedContactsBinaryDictionary extends ContactsBinaryDictionary { - private boolean mClosed; + private static final String NAME = "spellcheck_contacts"; + private final Object mLock = new Object(); public SynchronouslyLoadedContactsBinaryDictionary(final Context context, final Locale locale) { - super(context, locale); + super(context, locale, null /* dictFile */, NAME); } @Override - public synchronized ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer codes, - final String prevWordForBigrams, final ProximityInfo proximityInfo, - final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) { - reloadDictionaryIfRequired(); - return super.getSuggestions(codes, prevWordForBigrams, proximityInfo, blockOffensiveWords, - additionalFeaturesOptions); + public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer codes, + final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, + final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, + final int sessionId, final float[] inOutLanguageWeight) { + synchronized (mLock) { + return super.getSuggestions(codes, prevWordsInfo, proximityInfo, + blockOffensiveWords, additionalFeaturesOptions, sessionId, inOutLanguageWeight); + } } @Override - public synchronized boolean isValidWord(final String word) { - reloadDictionaryIfRequired(); - return isValidWordInner(word); - } - - // Protect against multiple closing - @Override - public synchronized void close() { - // Actually with the current implementation of ContactsDictionary it's safe to close - // several times, so the following protection is really only for foolproofing - if (mClosed) return; - mClosed = true; - super.close(); + public boolean isInDictionary(final String word) { + synchronized (mLock) { + return super.isInDictionary(word); + } } } diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/spellcheck/SynchronouslyLoadedUserBinaryDictionary.java index 6405b5e46..8c9d5d681 100644 --- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/SynchronouslyLoadedUserBinaryDictionary.java @@ -14,38 +14,47 @@ * limitations under the License. */ -package com.android.inputmethod.latin; +package com.android.inputmethod.latin.spellcheck; import android.content.Context; import com.android.inputmethod.keyboard.ProximityInfo; +import com.android.inputmethod.latin.PrevWordsInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import com.android.inputmethod.latin.UserBinaryDictionary; +import com.android.inputmethod.latin.WordComposer; import java.util.ArrayList; +import java.util.Locale; public final class SynchronouslyLoadedUserBinaryDictionary extends UserBinaryDictionary { + private static final String NAME = "spellcheck_user"; + private final Object mLock = new Object(); - public SynchronouslyLoadedUserBinaryDictionary(final Context context, final String locale) { - this(context, locale, false); + public SynchronouslyLoadedUserBinaryDictionary(final Context context, final Locale locale) { + this(context, locale, false /* alsoUseMoreRestrictiveLocales */); } - public SynchronouslyLoadedUserBinaryDictionary(final Context context, final String locale, + public SynchronouslyLoadedUserBinaryDictionary(final Context context, final Locale locale, final boolean alsoUseMoreRestrictiveLocales) { - super(context, locale, alsoUseMoreRestrictiveLocales); + super(context, locale, alsoUseMoreRestrictiveLocales, null /* dictFile */, NAME); } @Override - public synchronized ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer codes, - final String prevWordForBigrams, final ProximityInfo proximityInfo, - final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) { - reloadDictionaryIfRequired(); - return super.getSuggestions(codes, prevWordForBigrams, proximityInfo, blockOffensiveWords, - additionalFeaturesOptions); + public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer codes, + final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, + final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, + final int sessionId, final float[] inOutLanguageWeight) { + synchronized (mLock) { + return super.getSuggestions(codes, prevWordsInfo, proximityInfo, + blockOffensiveWords, additionalFeaturesOptions, sessionId, inOutLanguageWeight); + } } @Override - public synchronized boolean isValidWord(final String word) { - reloadDictionaryIfRequired(); - return isValidWordInner(word); + public boolean isInDictionary(final String word) { + synchronized (mLock) { + return super.isInDictionary(word); + } } } diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java index acd47450b..346aea34a 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java +++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java @@ -23,34 +23,27 @@ import android.graphics.drawable.Drawable; import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.Keyboard; -import com.android.inputmethod.keyboard.KeyboardActionListener; import com.android.inputmethod.keyboard.internal.KeyboardBuilder; import com.android.inputmethod.keyboard.internal.KeyboardIconsSet; import com.android.inputmethod.keyboard.internal.KeyboardParams; +import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.SuggestedWords; -import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.utils.TypefaceUtils; public final class MoreSuggestions extends Keyboard { - public static final int SUGGESTION_CODE_BASE = 1024; - public final SuggestedWords mSuggestedWords; - public static abstract class MoreSuggestionsListener extends KeyboardActionListener.Adapter { - public abstract void onSuggestionSelected(final int index, final SuggestedWordInfo info); - } - MoreSuggestions(final MoreSuggestionsParam params, final SuggestedWords suggestedWords) { super(params); mSuggestedWords = suggestedWords; } private static final class MoreSuggestionsParam extends KeyboardParams { - private final int[] mWidths = new int[SuggestionStripView.MAX_SUGGESTIONS]; - private final int[] mRowNumbers = new int[SuggestionStripView.MAX_SUGGESTIONS]; - private final int[] mColumnOrders = new int[SuggestionStripView.MAX_SUGGESTIONS]; - private final int[] mNumColumnsInRow = new int[SuggestionStripView.MAX_SUGGESTIONS]; + private final int[] mWidths = new int[SuggestedWords.MAX_SUGGESTIONS]; + private final int[] mRowNumbers = new int[SuggestedWords.MAX_SUGGESTIONS]; + private final int[] mColumnOrders = new int[SuggestedWords.MAX_SUGGESTIONS]; + private final int[] mNumColumnsInRow = new int[SuggestedWords.MAX_SUGGESTIONS]; private static final int MAX_COLUMNS_IN_ROW = 3; private int mNumRows; public Drawable mDivider; @@ -66,16 +59,23 @@ public final class MoreSuggestions extends Keyboard { clearKeys(); mDivider = res.getDrawable(R.drawable.more_suggestions_divider); mDividerWidth = mDivider.getIntrinsicWidth(); - final float padding = res.getDimension(R.dimen.more_suggestions_key_horizontal_padding); + final float padding = res.getDimension( + R.dimen.config_more_suggestions_key_horizontal_padding); int row = 0; int index = fromIndex; int rowStartIndex = fromIndex; - final int size = Math.min(suggestedWords.size(), SuggestionStripView.MAX_SUGGESTIONS); + final int size = Math.min(suggestedWords.size(), SuggestedWords.MAX_SUGGESTIONS); while (index < size) { - final String word = suggestedWords.getWord(index); + final String word; + if (isIndexSubjectToAutoCorrection(suggestedWords, index)) { + // INDEX_OF_AUTO_CORRECTION and INDEX_OF_TYPED_WORD got swapped. + word = suggestedWords.getLabel(SuggestedWords.INDEX_OF_TYPED_WORD); + } else { + word = suggestedWords.getLabel(index); + } // TODO: Should take care of text x-scaling. - mWidths[index] = (int)(TypefaceUtils.getLabelWidth(word, paint) + padding); + mWidths[index] = (int)(TypefaceUtils.getStringWidth(word, paint) + padding); final int numColumn = index - rowStartIndex + 1; final int columnWidth = (maxWidth - mDividerWidth * (numColumn - 1)) / numColumn; @@ -171,6 +171,11 @@ public final class MoreSuggestions extends Keyboard { } } + static boolean isIndexSubjectToAutoCorrection(final SuggestedWords suggestedWords, + final int index) { + return suggestedWords.mWillAutoCorrect && index == SuggestedWords.INDEX_OF_AUTO_CORRECTION; + } + public static final class Builder extends KeyboardBuilder<MoreSuggestionsParam> { private final MoreSuggestionsView mPaneView; private SuggestedWords mSuggestedWords; @@ -188,7 +193,6 @@ public final class MoreSuggestions extends Keyboard { final int xmlId = R.xml.kbd_suggestions_pane_template; load(xmlId, parentKeyboard.mId); mParams.mVerticalGap = mParams.mTopPadding = parentKeyboard.mVerticalGap / 2; - mPaneView.updateKeyboardGeometry(mParams.mDefaultRowHeight); final int count = mParams.layout(suggestedWords, fromIndex, maxWidth, minWidth, maxRow, mPaneView.newLabelPaint(null /* key */), mResources); @@ -205,13 +209,17 @@ public final class MoreSuggestions extends Keyboard { final int x = params.getX(index); final int y = params.getY(index); final int width = params.getWidth(index); - final String word = mSuggestedWords.getWord(index); - final String info = mSuggestedWords.getDebugString(index); - final int indexInMoreSuggestions = index + SUGGESTION_CODE_BASE; - final Key key = new Key( - params, word, info, KeyboardIconsSet.ICON_UNDEFINED, indexInMoreSuggestions, - null /* outputText */, x, y, width, params.mDefaultRowHeight, - 0 /* labelFlags */, Key.BACKGROUND_TYPE_NORMAL); + final String word; + final String info; + if (isIndexSubjectToAutoCorrection(mSuggestedWords, index)) { + // INDEX_OF_AUTO_CORRECTION and INDEX_OF_TYPED_WORD got swapped. + word = mSuggestedWords.getLabel(SuggestedWords.INDEX_OF_TYPED_WORD); + info = mSuggestedWords.getDebugString(SuggestedWords.INDEX_OF_TYPED_WORD); + } else { + word = mSuggestedWords.getLabel(index); + info = mSuggestedWords.getDebugString(index); + } + final Key key = new MoreSuggestionKey(word, info, index, params); params.markAsEdgeKey(key, index); params.onAddKey(key); final int columnNumber = params.getColumnNumber(index); @@ -226,6 +234,19 @@ public final class MoreSuggestions extends Keyboard { } } + static final class MoreSuggestionKey extends Key { + public final int mSuggestedWordIndex; + + public MoreSuggestionKey(final String word, final String info, final int index, + final MoreSuggestionsParam params) { + super(word /* label */, KeyboardIconsSet.ICON_UNDEFINED, Constants.CODE_OUTPUT_TEXT, + word /* outputText */, info, 0 /* labelFlags */, Key.BACKGROUND_TYPE_NORMAL, + params.getX(index), params.getY(index), params.getWidth(index), + params.mDefaultRowHeight, params.mHorizontalGap, params.mVerticalGap); + mSuggestedWordIndex = index; + } + } + private static final class Divider extends Key.Spacer { private final Drawable mIcon; diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java index 0ebe37782..f7b6f919d 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java +++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java @@ -20,11 +20,14 @@ import android.content.Context; import android.util.AttributeSet; import android.util.Log; +import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.keyboard.KeyboardActionListener; import com.android.inputmethod.keyboard.MoreKeysKeyboardView; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.SuggestedWords; -import com.android.inputmethod.latin.suggestions.MoreSuggestions.MoreSuggestionsListener; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import com.android.inputmethod.latin.suggestions.MoreSuggestions.MoreSuggestionKey; /** * A view that renders a virtual {@link MoreSuggestions}. It handles rendering of keys and detecting @@ -33,6 +36,10 @@ import com.android.inputmethod.latin.suggestions.MoreSuggestions.MoreSuggestions public final class MoreSuggestionsView extends MoreKeysKeyboardView { private static final String TAG = MoreSuggestionsView.class.getSimpleName(); + public static abstract class MoreSuggestionsListener extends KeyboardActionListener.Adapter { + public abstract void onSuggestionSelected(final SuggestedWordInfo info); + } + public MoreSuggestionsView(final Context context, final AttributeSet attrs) { this(context, attrs, R.attr.moreKeysKeyboardViewStyle); } @@ -42,6 +49,21 @@ public final class MoreSuggestionsView extends MoreKeysKeyboardView { super(context, attrs, defStyle); } + // TODO: Remove redundant override method. + @Override + public void setKeyboard(final Keyboard keyboard) { + super.setKeyboard(keyboard); + // With accessibility mode off, {@link #mAccessibilityDelegate} is set to null at the + // above {@link MoreKeysKeyboardView#setKeyboard(Keyboard)} call. + // With accessibility mode on, {@link #mAccessibilityDelegate} is set to a + // {@link MoreKeysKeyboardAccessibilityDelegate} object at the above + // {@link MoreKeysKeyboardView#setKeyboard(Keyboard)} call. + if (mAccessibilityDelegate != null) { + mAccessibilityDelegate.setOpenAnnounce(R.string.spoken_open_more_suggestions); + mAccessibilityDelegate.setCloseAnnounce(R.string.spoken_close_more_suggestions); + } + } + @Override protected int getDefaultCoordX() { final MoreSuggestions pane = (MoreSuggestions)getKeyboard(); @@ -54,12 +76,17 @@ public final class MoreSuggestionsView extends MoreKeysKeyboardView { public void adjustVerticalCorrectionForModalMode() { // Set vertical correction to zero (Reset more keys keyboard sliding allowance - // {@link R#dimen.more_keys_keyboard_slide_allowance}). + // {@link R#dimen.config_more_keys_keyboard_slide_allowance}). mKeyDetector.setKeyboard(getKeyboard(), -getPaddingLeft(), -getPaddingTop()); } @Override - public void onCodeInput(final int code, final int x, final int y) { + protected void onKeyInput(final Key key, final int x, final int y) { + if (!(key instanceof MoreSuggestionKey)) { + Log.e(TAG, "Expected key is MoreSuggestionKey, but found " + + key.getClass().getName()); + return; + } final Keyboard keyboard = getKeyboard(); if (!(keyboard instanceof MoreSuggestions)) { Log.e(TAG, "Expected keyboard is MoreSuggestions, but found " @@ -67,7 +94,7 @@ public final class MoreSuggestionsView extends MoreKeysKeyboardView { return; } final SuggestedWords suggestedWords = ((MoreSuggestions)keyboard).mSuggestedWords; - final int index = code - MoreSuggestions.SUGGESTION_CODE_BASE; + final int index = ((MoreSuggestionKey)key).mSuggestedWordIndex; if (index < 0 || index >= suggestedWords.size()) { Log.e(TAG, "Selected suggestion has an illegal index: " + index); return; @@ -77,7 +104,6 @@ public final class MoreSuggestionsView extends MoreKeysKeyboardView { + mListener.getClass().getName()); return; } - ((MoreSuggestionsListener)mListener).onSuggestionSelected( - index, suggestedWords.getInfo(index)); + ((MoreSuggestionsListener)mListener).onSuggestionSelected(suggestedWords.getInfo(index)); } } diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java index faa5560e4..ad5aad747 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java +++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java @@ -28,6 +28,7 @@ import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.support.v4.view.ViewCompat; import android.text.Spannable; import android.text.SpannableString; import android.text.Spanned; @@ -38,18 +39,20 @@ import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; import android.util.AttributeSet; import android.view.Gravity; -import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.TextView; +import com.android.inputmethod.accessibility.AccessibilityUtils; import com.android.inputmethod.latin.LatinImeLogger; +import com.android.inputmethod.latin.PunctuationSuggestions; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.SuggestedWords; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.utils.AutoCorrectionUtils; import com.android.inputmethod.latin.utils.ResourceUtils; +import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; import com.android.inputmethod.latin.utils.ViewLayoutUtils; import java.util.ArrayList; @@ -64,7 +67,7 @@ final class SuggestionStripLayoutHelper { public final int mPadding; public final int mDividerWidth; public final int mSuggestionsStripHeight; - public final int mSuggestionsCountInStrip; + private final int mSuggestionsCountInStrip; public final int mMoreSuggestionsRowHeight; private int mMaxMoreSuggestionsRow; public final float mMinMoreSuggestionsWidth; @@ -89,21 +92,18 @@ final class SuggestionStripLayoutHelper { private final Drawable mMoreSuggestionsHint; private static final String MORE_SUGGESTIONS_HINT = "\u2026"; private static final String LEFTWARDS_ARROW = "\u2190"; + private static final String RIGHTWARDS_ARROW = "\u2192"; private static final CharacterStyle BOLD_SPAN = new StyleSpan(Typeface.BOLD); private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan(); - private final int mSuggestionStripOption; + private final int mSuggestionStripOptions; // These constants are the flag values of - // {@link R.styleable#SuggestionStripView_suggestionStripOption} attribute. + // {@link R.styleable#SuggestionStripView_suggestionStripOptions} attribute. private static final int AUTO_CORRECT_BOLD = 0x01; private static final int AUTO_CORRECT_UNDERLINE = 0x02; private static final int VALID_TYPED_WORD_BOLD = 0x04; - private final TextView mWordToSaveView; - private final TextView mLeftwardsArrowView; - private final TextView mHintToSaveView; - public SuggestionStripLayoutHelper(final Context context, final AttributeSet attrs, final int defStyle, final ArrayList<TextView> wordViews, final ArrayList<View> dividerViews, final ArrayList<TextView> debugInfoViews) { @@ -119,12 +119,13 @@ final class SuggestionStripLayoutHelper { mDividerWidth = dividerView.getMeasuredWidth(); final Resources res = wordView.getResources(); - mSuggestionsStripHeight = res.getDimensionPixelSize(R.dimen.suggestions_strip_height); + mSuggestionsStripHeight = res.getDimensionPixelSize( + R.dimen.config_suggestions_strip_height); final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SuggestionStripView, defStyle, R.style.SuggestionStripView); - mSuggestionStripOption = a.getInt( - R.styleable.SuggestionStripView_suggestionStripOption, 0); + mSuggestionStripOptions = a.getInt( + R.styleable.SuggestionStripView_suggestionStripOptions, 0); mAlphaObsoleted = ResourceUtils.getFraction(a, R.styleable.SuggestionStripView_alphaObsoleted, 1.0f); mColorValidTypedWord = a.getColor(R.styleable.SuggestionStripView_colorValidTypedWord, 0); @@ -145,20 +146,17 @@ final class SuggestionStripLayoutHelper { a.recycle(); mMoreSuggestionsHint = getMoreSuggestionsHint(res, - res.getDimension(R.dimen.more_suggestions_hint_text_size), mColorAutoCorrect); + res.getDimension(R.dimen.config_more_suggestions_hint_text_size), + mColorAutoCorrect); mCenterPositionInStrip = mSuggestionsCountInStrip / 2; // Assuming there are at least three suggestions. Also, note that the suggestions are // laid out according to script direction, so this is left of the center for LTR scripts // and right of the center for RTL scripts. mTypedWordPositionWhenAutocorrect = mCenterPositionInStrip - 1; mMoreSuggestionsBottomGap = res.getDimensionPixelOffset( - R.dimen.more_suggestions_bottom_gap); - mMoreSuggestionsRowHeight = res.getDimensionPixelSize(R.dimen.more_suggestions_row_height); - - final LayoutInflater inflater = LayoutInflater.from(context); - mWordToSaveView = (TextView)inflater.inflate(R.layout.suggestion_word, null); - mLeftwardsArrowView = (TextView)inflater.inflate(R.layout.hint_add_to_dictionary, null); - mHintToSaveView = (TextView)inflater.inflate(R.layout.hint_add_to_dictionary, null); + R.dimen.config_more_suggestions_bottom_gap); + mMoreSuggestionsRowHeight = res.getDimensionPixelSize( + R.dimen.config_more_suggestions_row_height); } public int getMaxMoreSuggestionsRow() { @@ -203,23 +201,25 @@ final class SuggestionStripLayoutHelper { if (indexInSuggestedWords >= suggestedWords.size()) { return null; } - final String word = suggestedWords.getWord(indexInSuggestedWords); - final boolean isAutoCorrect = indexInSuggestedWords == 1 - && suggestedWords.willAutoCorrect(); - final boolean isTypedWordValid = indexInSuggestedWords == 0 - && suggestedWords.mTypedWordValid; - if (!isAutoCorrect && !isTypedWordValid) { + final String word = suggestedWords.getLabel(indexInSuggestedWords); + // TODO: don't use the index to decide whether this is the auto-correction/typed word, as + // this is brittle + final boolean isAutoCorrection = suggestedWords.mWillAutoCorrect + && indexInSuggestedWords == SuggestedWords.INDEX_OF_AUTO_CORRECTION; + final boolean isTypedWordValid = suggestedWords.mTypedWordValid + && indexInSuggestedWords == SuggestedWords.INDEX_OF_TYPED_WORD; + if (!isAutoCorrection && !isTypedWordValid) { return word; } final int len = word.length(); final Spannable spannedWord = new SpannableString(word); - final int option = mSuggestionStripOption; - if ((isAutoCorrect && (option & AUTO_CORRECT_BOLD) != 0) - || (isTypedWordValid && (option & VALID_TYPED_WORD_BOLD) != 0)) { + final int options = mSuggestionStripOptions; + if ((isAutoCorrection && (options & AUTO_CORRECT_BOLD) != 0) + || (isTypedWordValid && (options & VALID_TYPED_WORD_BOLD) != 0)) { spannedWord.setSpan(BOLD_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } - if (isAutoCorrect && (option & AUTO_CORRECT_UNDERLINE) != 0) { + if (isAutoCorrection && (options & AUTO_CORRECT_UNDERLINE) != 0) { spannedWord.setSpan(UNDERLINE_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } return spannedWord; @@ -229,7 +229,7 @@ final class SuggestionStripLayoutHelper { final SuggestedWords suggestedWords) { final int indexToDisplayMostImportantSuggestion; final int indexToDisplaySecondMostImportantSuggestion; - if (suggestedWords.willAutoCorrect()) { + if (suggestedWords.mWillAutoCorrect) { indexToDisplayMostImportantSuggestion = SuggestedWords.INDEX_OF_AUTO_CORRECTION; indexToDisplaySecondMostImportantSuggestion = SuggestedWords.INDEX_OF_TYPED_WORD; } else { @@ -246,35 +246,36 @@ final class SuggestionStripLayoutHelper { return indexInSuggestedWords; } - private int getSuggestionTextColor(final int indexInSuggestedWords, - final SuggestedWords suggestedWords) { + private int getSuggestionTextColor(final SuggestedWords suggestedWords, + final int indexInSuggestedWords) { final int positionInStrip = getPositionInSuggestionStrip(indexInSuggestedWords, suggestedWords); - // TODO: Need to revisit this logic with bigram suggestions - final boolean isSuggested = (indexInSuggestedWords != SuggestedWords.INDEX_OF_TYPED_WORD); + // Use identity for strings, not #equals : it's the typed word if it's the same object + final boolean isTypedWord = suggestedWords.getInfo(indexInSuggestedWords).isKindOf( + SuggestedWordInfo.KIND_TYPED); final int color; - if (positionInStrip == mCenterPositionInStrip && suggestedWords.willAutoCorrect()) { + if (positionInStrip == mCenterPositionInStrip && suggestedWords.mWillAutoCorrect) { color = mColorAutoCorrect; - } else if (positionInStrip == mCenterPositionInStrip && suggestedWords.mTypedWordValid) { + } else if (isTypedWord && suggestedWords.mTypedWordValid) { color = mColorValidTypedWord; - } else if (isSuggested) { - color = mColorSuggested; - } else { + } else if (isTypedWord) { color = mColorTypedWord; + } else { + color = mColorSuggested; } if (LatinImeLogger.sDBG && suggestedWords.size() > 1) { // If we auto-correct, then the autocorrection is in slot 0 and the typed word // is in slot 1. if (positionInStrip == mCenterPositionInStrip && AutoCorrectionUtils.shouldBlockAutoCorrectionBySafetyNet( - suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION), - suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD))) { + suggestedWords.getLabel(SuggestedWords.INDEX_OF_AUTO_CORRECTION), + suggestedWords.getLabel(SuggestedWords.INDEX_OF_TYPED_WORD))) { return 0xFFFF0000; } } - if (suggestedWords.mIsObsoleteSuggestions && isSuggested) { + if (suggestedWords.mIsObsoleteSuggestions && !isTypedWord) { return applyAlpha(color, mAlphaObsoleted); } return color; @@ -292,54 +293,64 @@ final class SuggestionStripLayoutHelper { params.gravity = Gravity.CENTER; } - public void layout(final SuggestedWords suggestedWords, final ViewGroup stripView, - final ViewGroup placerView) { - if (suggestedWords.mIsPunctuationSuggestions) { - layoutPunctuationSuggestions(suggestedWords, stripView); - return; + /** + * Layout suggestions to the suggestions strip. And returns the number of suggestions displayed + * in the suggestions strip. + * + * @param suggestedWords suggestions to be shown in the suggestions strip. + * @param stripView the suggestions strip view. + * @param placerView the view where the debug info will be placed. + * @return the number of suggestions displayed in the suggestions strip + */ + public int layoutAndReturnSuggestionCountInStrip(final SuggestedWords suggestedWords, + final ViewGroup stripView, final ViewGroup placerView) { + if (suggestedWords.isPunctuationSuggestions()) { + return layoutPunctuationSuggestionsAndReturnSuggestionCountInStrip( + (PunctuationSuggestions)suggestedWords, stripView); } - final int countInStrip = mSuggestionsCountInStrip; - setupWordViewsTextAndColor(suggestedWords, countInStrip); + setupWordViewsTextAndColor(suggestedWords, mSuggestionsCountInStrip); final TextView centerWordView = mWordViews.get(mCenterPositionInStrip); - final int availableStripWidth = placerView.getWidth() - - placerView.getPaddingRight() - placerView.getPaddingLeft(); - final int centerWidth = getSuggestionWidth(mCenterPositionInStrip, availableStripWidth); - if (getTextScaleX(centerWordView.getText(), centerWidth, centerWordView.getPaint()) - < MIN_TEXT_XSCALE) { + final int stripWidth = stripView.getWidth(); + final int centerWidth = getSuggestionWidth(mCenterPositionInStrip, stripWidth); + final int countInStrip; + if (suggestedWords.size() == 1 || getTextScaleX(centerWordView.getText(), centerWidth, + centerWordView.getPaint()) < MIN_TEXT_XSCALE) { // Layout only the most relevant suggested word at the center of the suggestion strip // by consolidating all slots in the strip. - mMoreSuggestionsAvailable = (suggestedWords.size() > 1); - layoutWord(mCenterPositionInStrip, availableStripWidth - mPadding); + countInStrip = 1; + mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip); + layoutWord(mCenterPositionInStrip, stripWidth - mPadding); stripView.addView(centerWordView); setLayoutWeight(centerWordView, 1.0f, ViewGroup.LayoutParams.MATCH_PARENT); if (SuggestionStripView.DBG) { - layoutDebugInfo(mCenterPositionInStrip, placerView, availableStripWidth); + layoutDebugInfo(mCenterPositionInStrip, placerView, stripWidth); } - return; - } - - mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip); - int x = 0; - for (int positionInStrip = 0; positionInStrip < countInStrip; positionInStrip++) { - if (positionInStrip != 0) { - final View divider = mDividerViews.get(positionInStrip); - // Add divider if this isn't the left most suggestion in suggestions strip. - addDivider(stripView, divider); - x += divider.getMeasuredWidth(); - } - - final int width = getSuggestionWidth(positionInStrip, availableStripWidth); - final TextView wordView = layoutWord(positionInStrip, width); - stripView.addView(wordView); - setLayoutWeight(wordView, getSuggestionWeight(positionInStrip), - ViewGroup.LayoutParams.MATCH_PARENT); - x += wordView.getMeasuredWidth(); - - if (SuggestionStripView.DBG) { - layoutDebugInfo(positionInStrip, placerView, x); + } else { + countInStrip = mSuggestionsCountInStrip; + mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip); + int x = 0; + for (int positionInStrip = 0; positionInStrip < countInStrip; positionInStrip++) { + if (positionInStrip != 0) { + final View divider = mDividerViews.get(positionInStrip); + // Add divider if this isn't the left most suggestion in suggestions strip. + addDivider(stripView, divider); + x += divider.getMeasuredWidth(); + } + + final int width = getSuggestionWidth(positionInStrip, stripWidth); + final TextView wordView = layoutWord(positionInStrip, width); + stripView.addView(wordView); + setLayoutWeight(wordView, getSuggestionWeight(positionInStrip), + ViewGroup.LayoutParams.MATCH_PARENT); + x += wordView.getMeasuredWidth(); + + if (SuggestionStripView.DBG) { + layoutDebugInfo(positionInStrip, placerView, x); + } } } + return countInStrip; } /** @@ -370,13 +381,19 @@ final class SuggestionStripLayoutHelper { } else { wordView.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); } - - // Disable this suggestion if the suggestion is null or empty. - wordView.setEnabled(!TextUtils.isEmpty(word)); + // {@link StyleSpan} in a content description may cause an issue of TTS/TalkBack. + // Use a simple {@link String} to avoid the issue. + wordView.setContentDescription(TextUtils.isEmpty(word) ? null : word.toString()); final CharSequence text = getEllipsizedText(word, width, wordView.getPaint()); final float scaleX = getTextScaleX(word, width, wordView.getPaint()); wordView.setText(text); // TextView.setText() resets text scale x to 1.0. wordView.setTextScaleX(Math.max(scaleX, MIN_TEXT_XSCALE)); + // A <code>wordView</code> should be disabled when <code>word</code> is empty in order to + // make it unclickable. + // With accessibility touch exploration on, <code>wordView</code> should be enabled even + // when it is empty to avoid announcing as "disabled". + wordView.setEnabled(!TextUtils.isEmpty(word) + || AccessibilityUtils.getInstance().isTouchExplorationEnabled()); return wordView; } @@ -415,7 +432,9 @@ final class SuggestionStripLayoutHelper { final int countInStrip) { // Clear all suggestions first for (int positionInStrip = 0; positionInStrip < countInStrip; ++positionInStrip) { - mWordViews.get(positionInStrip).setText(null); + final TextView wordView = mWordViews.get(positionInStrip); + wordView.setText(null); + wordView.setTag(null); // Make this inactive for touches in {@link #layoutWord(int,int)}. if (SuggestionStripView.DBG) { mDebugInfoViews.get(positionInStrip).setText(null); @@ -431,7 +450,7 @@ final class SuggestionStripLayoutHelper { // {@link SuggestionStripView#onClick(View)}. wordView.setTag(indexInSuggestedWords); wordView.setText(getStyledSuggestedWord(suggestedWords, indexInSuggestedWords)); - wordView.setTextColor(getSuggestionTextColor(positionInStrip, suggestedWords)); + wordView.setTextColor(getSuggestionTextColor(suggestedWords, indexInSuggestedWords)); if (SuggestionStripView.DBG) { mDebugInfoViews.get(positionInStrip).setText( suggestedWords.getDebugString(indexInSuggestedWords)); @@ -439,9 +458,9 @@ final class SuggestionStripLayoutHelper { } } - private void layoutPunctuationSuggestions(final SuggestedWords suggestedWords, - final ViewGroup stripView) { - final int countInStrip = Math.min(suggestedWords.size(), PUNCTUATIONS_IN_STRIP); + private int layoutPunctuationSuggestionsAndReturnSuggestionCountInStrip( + final PunctuationSuggestions punctuationSuggestions, final ViewGroup stripView) { + final int countInStrip = Math.min(punctuationSuggestions.size(), PUNCTUATIONS_IN_STRIP); for (int positionInStrip = 0; positionInStrip < countInStrip; positionInStrip++) { if (positionInStrip != 0) { // Add divider if this isn't the left most suggestion in suggestions strip. @@ -449,71 +468,69 @@ final class SuggestionStripLayoutHelper { } final TextView wordView = mWordViews.get(positionInStrip); - wordView.setEnabled(true); - wordView.setTextColor(mColorAutoCorrect); + final String punctuation = punctuationSuggestions.getLabel(positionInStrip); // {@link TextView#getTag()} is used to get the index in suggestedWords at // {@link SuggestionStripView#onClick(View)}. wordView.setTag(positionInStrip); - wordView.setText(suggestedWords.getWord(positionInStrip)); + wordView.setText(punctuation); + wordView.setContentDescription(punctuation); wordView.setTextScaleX(1.0f); wordView.setCompoundDrawables(null, null, null, null); + wordView.setTextColor(mColorAutoCorrect); stripView.addView(wordView); setLayoutWeight(wordView, 1.0f, mSuggestionsStripHeight); } - mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip); + mMoreSuggestionsAvailable = (punctuationSuggestions.size() > countInStrip); + return countInStrip; } - public void layoutAddToDictionaryHint(final String word, final ViewGroup stripView, - final int stripWidth, final CharSequence hintText, final OnClickListener listener) { + public void layoutAddToDictionaryHint(final String word, final ViewGroup addToDictionaryStrip) { + final int stripWidth = addToDictionaryStrip.getWidth(); final int width = stripWidth - mDividerWidth - mPadding * 2; - final TextView wordView = mWordToSaveView; + final TextView wordView = (TextView)addToDictionaryStrip.findViewById(R.id.word_to_save); wordView.setTextColor(mColorTypedWord); final int wordWidth = (int)(width * mCenterSuggestionWeight); - final CharSequence text = getEllipsizedText(word, wordWidth, wordView.getPaint()); + final CharSequence wordToSave = getEllipsizedText(word, wordWidth, wordView.getPaint()); final float wordScaleX = wordView.getTextScaleX(); - // {@link TextView#setTag()} is used to hold the word to be added to dictionary. The word - // will be extracted at {@link #getAddToDictionaryWord()}. - wordView.setTag(word); - wordView.setText(text); + wordView.setText(wordToSave); wordView.setTextScaleX(wordScaleX); - stripView.addView(wordView); setLayoutWeight(wordView, mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT); - stripView.addView(mDividerViews.get(0)); - - final TextView leftArrowView = mLeftwardsArrowView; - leftArrowView.setTextColor(mColorAutoCorrect); - leftArrowView.setText(LEFTWARDS_ARROW); - stripView.addView(leftArrowView); - - final TextView hintView = mHintToSaveView; - hintView.setGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL); + final TextView hintView = (TextView)addToDictionaryStrip.findViewById( + R.id.hint_add_to_dictionary); hintView.setTextColor(mColorAutoCorrect); - final int hintWidth = width - wordWidth - leftArrowView.getWidth(); - final float hintScaleX = getTextScaleX(hintText, hintWidth, hintView.getPaint()); - hintView.setText(hintText); + final boolean isRtlLanguage = (ViewCompat.getLayoutDirection(addToDictionaryStrip) + == ViewCompat.LAYOUT_DIRECTION_RTL); + final String arrow = isRtlLanguage ? RIGHTWARDS_ARROW : LEFTWARDS_ARROW; + final Resources res = addToDictionaryStrip.getResources(); + final boolean isRtlSystem = SubtypeLocaleUtils.isRtlLanguage(res.getConfiguration().locale); + final CharSequence hintText = res.getText(R.string.hint_add_to_dictionary); + final String hintWithArrow = (isRtlLanguage == isRtlSystem) + ? (arrow + hintText) : (hintText + arrow); + final int hintWidth = width - wordWidth; + hintView.setTextScaleX(1.0f); // Reset textScaleX. + final float hintScaleX = getTextScaleX(hintWithArrow, hintWidth, hintView.getPaint()); + hintView.setText(hintWithArrow); hintView.setTextScaleX(hintScaleX); - stripView.addView(hintView); setLayoutWeight( hintView, 1.0f - mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT); - - wordView.setOnClickListener(listener); - leftArrowView.setOnClickListener(listener); - hintView.setOnClickListener(listener); - } - - public String getAddToDictionaryWord() { - // String tag is set at - // {@link #layoutAddToDictionaryHint(String,ViewGroup,int,CharSequence,OnClickListener}. - return (String)mWordToSaveView.getTag(); } - public boolean isAddToDictionaryShowing(final View v) { - return v == mWordToSaveView || v == mHintToSaveView || v == mLeftwardsArrowView; + public void layoutImportantNotice(final View importantNoticeStrip, + final String importantNoticeTitle) { + final TextView titleView = (TextView)importantNoticeStrip.findViewById( + R.id.important_notice_title); + final int width = titleView.getWidth() - titleView.getPaddingLeft() + - titleView.getPaddingRight(); + titleView.setTextColor(mColorAutoCorrect); + titleView.setText(importantNoticeTitle); + titleView.setTextScaleX(1.0f); // Reset textScaleX. + final float titleScaleX = getTextScaleX(importantNoticeTitle, width, titleView.getPaint()); + titleView.setTextScaleX(titleScaleX); } - private static void setLayoutWeight(final View v, final float weight, final int height) { + static void setLayoutWeight(final View v, final float weight, final int height) { final ViewGroup.LayoutParams lp = v.getLayoutParams(); if (lp instanceof LinearLayout.LayoutParams) { final LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams)lp; @@ -527,7 +544,7 @@ final class SuggestionStripLayoutHelper { final TextPaint paint) { paint.setTextScaleX(1.0f); final int width = getTextWidth(text, paint); - if (width <= maxWidth) { + if (width <= maxWidth || maxWidth <= 0) { return 1.0f; } return maxWidth / (float)width; diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java index 75f17c559..8654e12a9 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java +++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java @@ -18,7 +18,13 @@ package com.android.inputmethod.latin.suggestions; import android.content.Context; import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.support.v4.view.ViewCompat; +import android.text.TextUtils; import android.util.AttributeSet; +import android.util.TypedValue; import android.view.GestureDetector; import android.view.LayoutInflater; import android.view.MotionEvent; @@ -26,11 +32,13 @@ import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.widget.ImageButton; import android.widget.RelativeLayout; import android.widget.TextView; import com.android.inputmethod.keyboard.Keyboard; -import com.android.inputmethod.keyboard.KeyboardSwitcher; import com.android.inputmethod.keyboard.MainKeyboardView; import com.android.inputmethod.keyboard.MoreKeysPanel; import com.android.inputmethod.latin.AudioAndHapticFeedbackManager; @@ -39,10 +47,10 @@ import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; -import com.android.inputmethod.latin.define.ProductionFlag; -import com.android.inputmethod.latin.suggestions.MoreSuggestions.MoreSuggestionsListener; -import com.android.inputmethod.latin.utils.CollectionUtils; -import com.android.inputmethod.research.ResearchLogger; +import com.android.inputmethod.latin.settings.Settings; +import com.android.inputmethod.latin.settings.SettingsValues; +import com.android.inputmethod.latin.suggestions.MoreSuggestionsView.MoreSuggestionsListener; +import com.android.inputmethod.latin.utils.ImportantNoticeUtils; import java.util.ArrayList; @@ -50,29 +58,82 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick OnLongClickListener { public interface Listener { public void addWordToUserDictionary(String word); - public void pickSuggestionManually(int index, SuggestedWordInfo word); + public void showImportantNoticeContents(); + public void pickSuggestionManually(SuggestedWordInfo word); + public void onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat); } - // The maximum number of suggestions available. See {@link Suggest#mPrefMaxSuggestions}. - public static final int MAX_SUGGESTIONS = 18; - static final boolean DBG = LatinImeLogger.sDBG; + private static final float DEBUG_INFO_TEXT_SIZE_IN_DIP = 6.0f; private final ViewGroup mSuggestionsStrip; + private final ImageButton mVoiceKey; + private final ViewGroup mAddToDictionaryStrip; + private final View mImportantNoticeStrip; MainKeyboardView mMainKeyboardView; private final View mMoreSuggestionsContainer; private final MoreSuggestionsView mMoreSuggestionsView; private final MoreSuggestions.Builder mMoreSuggestionsBuilder; - private final ArrayList<TextView> mWordViews = CollectionUtils.newArrayList(); - private final ArrayList<TextView> mDebugInfoViews = CollectionUtils.newArrayList(); - private final ArrayList<View> mDividerViews = CollectionUtils.newArrayList(); + private final ArrayList<TextView> mWordViews = new ArrayList<>(); + private final ArrayList<TextView> mDebugInfoViews = new ArrayList<>(); + private final ArrayList<View> mDividerViews = new ArrayList<>(); Listener mListener; private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY; + private int mSuggestionsCountInStrip; private final SuggestionStripLayoutHelper mLayoutHelper; + private final StripVisibilityGroup mStripVisibilityGroup; + + private static class StripVisibilityGroup { + private final View mSuggestionStripView; + private final View mSuggestionsStrip; + private final View mAddToDictionaryStrip; + private final View mImportantNoticeStrip; + + public StripVisibilityGroup(final View suggestionStripView, + final ViewGroup suggestionsStrip, final ViewGroup addToDictionaryStrip, + final View importantNoticeStrip) { + mSuggestionStripView = suggestionStripView; + mSuggestionsStrip = suggestionsStrip; + mAddToDictionaryStrip = addToDictionaryStrip; + mImportantNoticeStrip = importantNoticeStrip; + showSuggestionsStrip(); + } + + public void setLayoutDirection(final boolean isRtlLanguage) { + final int layoutDirection = isRtlLanguage ? ViewCompat.LAYOUT_DIRECTION_RTL + : ViewCompat.LAYOUT_DIRECTION_LTR; + ViewCompat.setLayoutDirection(mSuggestionStripView, layoutDirection); + ViewCompat.setLayoutDirection(mSuggestionsStrip, layoutDirection); + ViewCompat.setLayoutDirection(mAddToDictionaryStrip, layoutDirection); + ViewCompat.setLayoutDirection(mImportantNoticeStrip, layoutDirection); + } + + public void showSuggestionsStrip() { + mSuggestionsStrip.setVisibility(VISIBLE); + mAddToDictionaryStrip.setVisibility(INVISIBLE); + mImportantNoticeStrip.setVisibility(INVISIBLE); + } + + public void showAddToDictionaryStrip() { + mSuggestionsStrip.setVisibility(INVISIBLE); + mAddToDictionaryStrip.setVisibility(VISIBLE); + mImportantNoticeStrip.setVisibility(INVISIBLE); + } + + public void showImportantNoticeStrip() { + mSuggestionsStrip.setVisibility(INVISIBLE); + mAddToDictionaryStrip.setVisibility(INVISIBLE); + mImportantNoticeStrip.setVisibility(VISIBLE); + } + + public boolean isShowingAddToDictionaryStrip() { + return mAddToDictionaryStrip.getVisibility() == VISIBLE; + } + } /** * Construct a {@link SuggestionStripView} for showing suggestions to be picked by the user. @@ -91,15 +152,23 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick inflater.inflate(R.layout.suggestions_strip, this); mSuggestionsStrip = (ViewGroup)findViewById(R.id.suggestions_strip); - for (int pos = 0; pos < MAX_SUGGESTIONS; pos++) { - final TextView word = (TextView)inflater.inflate(R.layout.suggestion_word, null); + mVoiceKey = (ImageButton)findViewById(R.id.suggestions_strip_voice_key); + mAddToDictionaryStrip = (ViewGroup)findViewById(R.id.add_to_dictionary_strip); + mImportantNoticeStrip = findViewById(R.id.important_notice_strip); + mStripVisibilityGroup = new StripVisibilityGroup(this, mSuggestionsStrip, + mAddToDictionaryStrip, mImportantNoticeStrip); + + for (int pos = 0; pos < SuggestedWords.MAX_SUGGESTIONS; pos++) { + final TextView word = new TextView(context, null, R.attr.suggestionWordStyle); word.setOnClickListener(this); word.setOnLongClickListener(this); mWordViews.add(word); final View divider = inflater.inflate(R.layout.suggestion_divider, null); - divider.setOnClickListener(this); mDividerViews.add(divider); - mDebugInfoViews.add((TextView)inflater.inflate(R.layout.suggestion_info, null)); + final TextView info = new TextView(context, null, R.attr.suggestionWordStyle); + info.setTextColor(Color.WHITE); + info.setTextSize(TypedValue.COMPLEX_UNIT_DIP, DEBUG_INFO_TEXT_SIZE_IN_DIP); + mDebugInfoViews.add(info); } mLayoutHelper = new SuggestionStripLayoutHelper( @@ -112,9 +181,16 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick final Resources res = context.getResources(); mMoreSuggestionsModalTolerance = res.getDimensionPixelOffset( - R.dimen.more_suggestions_modal_tolerance); + R.dimen.config_more_suggestions_modal_tolerance); mMoreSuggestionsSlidingDetector = new GestureDetector( context, mMoreSuggestionsSlidingListener); + + final TypedArray keyboardAttr = context.obtainStyledAttributes(attrs, + R.styleable.Keyboard, defStyle, R.style.SuggestionStripView); + final Drawable iconVoice = keyboardAttr.getDrawable(R.styleable.Keyboard_iconShortcutKey); + keyboardAttr.recycle(); + mVoiceKey.setImageDrawable(iconVoice); + mVoiceKey.setOnClickListener(this); } /** @@ -126,13 +202,20 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick mMainKeyboardView = (MainKeyboardView)inputView.findViewById(R.id.keyboard_view); } - public void setSuggestions(final SuggestedWords suggestedWords) { + public void updateVisibility(final boolean shouldBeVisible, final boolean isFullscreenMode) { + final int visibility = shouldBeVisible ? VISIBLE : (isFullscreenMode ? GONE : INVISIBLE); + setVisibility(visibility); + final SettingsValues currentSettingsValues = Settings.getInstance().getCurrent(); + mVoiceKey.setVisibility(currentSettingsValues.mShowsVoiceInputKey ? VISIBLE : INVISIBLE); + } + + public void setSuggestions(final SuggestedWords suggestedWords, final boolean isRtlLanguage) { clear(); + mStripVisibilityGroup.setLayoutDirection(isRtlLanguage); mSuggestedWords = suggestedWords; - mLayoutHelper.layout(mSuggestedWords, mSuggestionsStrip, this); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.suggestionStripView_setSuggestions(mSuggestedWords); - } + mSuggestionsCountInStrip = mLayoutHelper.layoutAndReturnSuggestionCountInStrip( + mSuggestedWords, mSuggestionsStrip, this); + mStripVisibilityGroup.showSuggestionsStrip(); } public int setMoreSuggestionsHeight(final int remainingHeight) { @@ -140,14 +223,16 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick } public boolean isShowingAddToDictionaryHint() { - return mSuggestionsStrip.getChildCount() > 0 - && mLayoutHelper.isAddToDictionaryShowing(mSuggestionsStrip.getChildAt(0)); + return mStripVisibilityGroup.isShowingAddToDictionaryStrip(); } - public void showAddToDictionaryHint(final String word, final CharSequence hintText) { - clear(); - mLayoutHelper.layoutAddToDictionaryHint( - word, mSuggestionsStrip, getWidth(), hintText, this); + public void showAddToDictionaryHint(final String word) { + mLayoutHelper.layoutAddToDictionaryHint(word, mAddToDictionaryStrip); + // {@link TextView#setTag()} is used to hold the word to be added to dictionary. The word + // will be extracted at {@link #onClick(View)}. + mAddToDictionaryStrip.setTag(word); + mAddToDictionaryStrip.setOnClickListener(this); + mStripVisibilityGroup.showAddToDictionaryStrip(); } public boolean dismissAddToDictionaryHint() { @@ -158,31 +243,65 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick return false; } + // This method checks if we should show the important notice (checks on permanent storage if + // it has been shown once already or not, and if in the setup wizard). If applicable, it shows + // the notice. In all cases, it returns true if it was shown, false otherwise. + public boolean maybeShowImportantNoticeTitle() { + if (!ImportantNoticeUtils.shouldShowImportantNotice(getContext())) { + return false; + } + if (getWidth() <= 0) { + return false; + } + final String importantNoticeTitle = ImportantNoticeUtils.getNextImportantNoticeTitle( + getContext()); + if (TextUtils.isEmpty(importantNoticeTitle)) { + return false; + } + if (isShowingMoreSuggestionPanel()) { + dismissMoreSuggestionsPanel(); + } + mLayoutHelper.layoutImportantNotice(mImportantNoticeStrip, importantNoticeTitle); + mStripVisibilityGroup.showImportantNoticeStrip(); + mImportantNoticeStrip.setOnClickListener(this); + return true; + } + public void clear() { mSuggestionsStrip.removeAllViews(); - removeAllViews(); - addView(mSuggestionsStrip); - mMoreSuggestionsView.dismissMoreKeysPanel(); + removeAllDebugInfoViews(); + mStripVisibilityGroup.showSuggestionsStrip(); + dismissMoreSuggestionsPanel(); + } + + private void removeAllDebugInfoViews() { + // The debug info views may be placed as children views of this {@link SuggestionStripView}. + for (final View debugInfoView : mDebugInfoViews) { + final ViewParent parent = debugInfoView.getParent(); + if (parent instanceof ViewGroup) { + ((ViewGroup)parent).removeView(debugInfoView); + } + } } private final MoreSuggestionsListener mMoreSuggestionsListener = new MoreSuggestionsListener() { @Override - public void onSuggestionSelected(final int index, final SuggestedWordInfo wordInfo) { - mListener.pickSuggestionManually(index, wordInfo); - mMoreSuggestionsView.dismissMoreKeysPanel(); + public void onSuggestionSelected(final SuggestedWordInfo wordInfo) { + mListener.pickSuggestionManually(wordInfo); + dismissMoreSuggestionsPanel(); } @Override public void onCancelInput() { - mMoreSuggestionsView.dismissMoreKeysPanel(); + dismissMoreSuggestionsPanel(); } }; private final MoreKeysPanel.Controller mMoreSuggestionsController = new MoreKeysPanel.Controller() { @Override - public void onDismissMoreKeysPanel(final MoreKeysPanel panel) { - mMainKeyboardView.onDismissMoreKeysPanel(panel); + public void onDismissMoreKeysPanel() { + mMainKeyboardView.onDismissMoreKeysPanel(); } @Override @@ -191,11 +310,19 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick } @Override - public void onCancelMoreKeysPanel(final MoreKeysPanel panel) { - mMoreSuggestionsView.dismissMoreKeysPanel(); + public void onCancelMoreKeysPanel() { + dismissMoreSuggestionsPanel(); } }; + public boolean isShowingMoreSuggestionPanel() { + return mMoreSuggestionsView.isShowingInParent(); + } + + public void dismissMoreSuggestionsPanel() { + mMoreSuggestionsView.dismissMoreKeysPanel(); + } + @Override public boolean onLongClick(final View view) { AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback( @@ -204,7 +331,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick } boolean showMoreSuggestions() { - final Keyboard parentKeyboard = KeyboardSwitcher.getInstance().getKeyboard(); + final Keyboard parentKeyboard = mMainKeyboardView.getKeyboard(); if (parentKeyboard == null) { return false; } @@ -212,11 +339,17 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick if (!layoutHelper.mMoreSuggestionsAvailable) { return false; } + // Dismiss another {@link MoreKeysPanel} that may be being showed, for example + // {@link MoreKeysKeyboardView}. + mMainKeyboardView.onDismissMoreKeysPanel(); + // Dismiss all key previews and sliding key input preview that may be being showed. + mMainKeyboardView.dismissAllKeyPreviews(); + mMainKeyboardView.dismissSlidingKeyInputPreview(); final int stripWidth = getWidth(); final View container = mMoreSuggestionsContainer; final int maxWidth = stripWidth - container.getPaddingLeft() - container.getPaddingRight(); final MoreSuggestions.Builder builder = mMoreSuggestionsBuilder; - builder.layout(mSuggestedWords, layoutHelper.mSuggestionsCountInStrip, maxWidth, + builder.layout(mSuggestedWords, mSuggestionsCountInStrip, maxWidth, (int)(maxWidth * layoutHelper.mMinMoreSuggestionsWidth), layoutHelper.getMaxMoreSuggestionsRow(), parentKeyboard); mMoreSuggestionsView.setKeyboard(builder.build()); @@ -227,20 +360,16 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick final int pointY = -layoutHelper.mMoreSuggestionsBottomGap; moreKeysPanel.showMoreKeysPanel(this, mMoreSuggestionsController, pointX, pointY, mMoreSuggestionsListener); - mMoreSuggestionsMode = MORE_SUGGESTIONS_CHECKING_MODAL_OR_SLIDING; mOriginX = mLastX; mOriginY = mLastY; - for (int i = 0; i < layoutHelper.mSuggestionsCountInStrip; i++) { + for (int i = 0; i < mSuggestionsCountInStrip; i++) { mWordViews.get(i).setPressed(false); } return true; } - // Working variables for onLongClick and dispatchTouchEvent. - private int mMoreSuggestionsMode = MORE_SUGGESTIONS_IN_MODAL_MODE; - private static final int MORE_SUGGESTIONS_IN_MODAL_MODE = 0; - private static final int MORE_SUGGESTIONS_CHECKING_MODAL_OR_SLIDING = 1; - private static final int MORE_SUGGESTIONS_IN_SLIDING_MODE = 2; + // Working variables for {@link #onLongClick(View)} and + // {@link onInterceptTouchEvent(MotionEvent)}. private int mLastX; private int mLastY; private int mOriginX; @@ -260,36 +389,45 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick }; @Override - public boolean dispatchTouchEvent(final MotionEvent me) { + public boolean onInterceptTouchEvent(final MotionEvent me) { if (!mMoreSuggestionsView.isShowingInParent()) { mLastX = (int)me.getX(); mLastY = (int)me.getY(); - if (mMoreSuggestionsSlidingDetector.onTouchEvent(me)) { - return true; - } - return super.dispatchTouchEvent(me); + return mMoreSuggestionsSlidingDetector.onTouchEvent(me); } final int action = me.getAction(); final int index = me.getActionIndex(); final int x = (int)me.getX(index); final int y = (int)me.getY(index); - - if (mMoreSuggestionsMode == MORE_SUGGESTIONS_CHECKING_MODAL_OR_SLIDING) { - if (Math.abs(x - mOriginX) >= mMoreSuggestionsModalTolerance - || mOriginY - y >= mMoreSuggestionsModalTolerance) { - // Decided to be in the sliding input mode only when the touch point has been moved - // upward. - mMoreSuggestionsMode = MORE_SUGGESTIONS_IN_SLIDING_MODE; - } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { - // Decided to be in the modal input mode - mMoreSuggestionsMode = MORE_SUGGESTIONS_IN_MODAL_MODE; - mMoreSuggestionsView.adjustVerticalCorrectionForModalMode(); - } + if (Math.abs(x - mOriginX) >= mMoreSuggestionsModalTolerance + || mOriginY - y >= mMoreSuggestionsModalTolerance) { + // Decided to be in the sliding input mode only when the touch point has been moved + // upward. Further {@link MotionEvent}s will be delivered to + // {@link #onTouchEvent(MotionEvent)}. return true; } - // MORE_SUGGESTIONS_IN_SLIDING_MODE + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { + // Decided to be in the modal input mode. + mMoreSuggestionsView.adjustVerticalCorrectionForModalMode(); + } + return false; + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(final AccessibilityEvent event) { + // Don't populate accessibility event with suggested words and voice key. + return true; + } + + @Override + public boolean onTouchEvent(final MotionEvent me) { + // In the sliding input mode. {@link MotionEvent} should be forwarded to + // {@link MoreSuggestionsView}. + final int index = me.getActionIndex(); + final int x = (int)me.getX(index); + final int y = (int)me.getY(index); me.setLocation(mMoreSuggestionsView.translateX(x), mMoreSuggestionsView.translateY(y)); mMoreSuggestionsView.onTouchEvent(me); return true; @@ -297,31 +435,52 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick @Override public void onClick(final View view) { - if (mLayoutHelper.isAddToDictionaryShowing(view)) { - mListener.addWordToUserDictionary(mLayoutHelper.getAddToDictionaryWord()); - clear(); + AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback( + Constants.CODE_UNSPECIFIED, this); + if (view == mImportantNoticeStrip) { + mListener.showImportantNoticeContents(); return; } - - final Object tag = view.getTag(); - // Integer tag is set at - // {@link SuggestionStripLayoutHelper#setupWordViewsTextAndColor(SuggestedWords,int)} and - // {@link SuggestionStripLayoutHelper#layoutPunctuationSuggestions(SuggestedWords,ViewGroup} - if (!(tag instanceof Integer)) { + if (view == mVoiceKey) { + mListener.onCodeInput(Constants.CODE_SHORTCUT, + Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE, + false /* isKeyRepeat */); return; } - final int index = (Integer) tag; - if (index >= mSuggestedWords.size()) { + final Object tag = view.getTag(); + // {@link String} tag is set at {@link #showAddToDictionaryHint(String,CharSequence)}. + if (tag instanceof String) { + final String wordToSave = (String)tag; + mListener.addWordToUserDictionary(wordToSave); + clear(); return; } - final SuggestedWordInfo wordInfo = mSuggestedWords.getInfo(index); - mListener.pickSuggestionManually(index, wordInfo); + // {@link Integer} tag is set at + // {@link SuggestionStripLayoutHelper#setupWordViewsTextAndColor(SuggestedWords,int)} and + // {@link SuggestionStripLayoutHelper#layoutPunctuationSuggestions(SuggestedWords,ViewGroup} + if (tag instanceof Integer) { + final int index = (Integer) tag; + if (index >= mSuggestedWords.size()) { + return; + } + final SuggestedWordInfo wordInfo = mSuggestedWords.getInfo(index); + mListener.pickSuggestionManually(wordInfo); + } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - mMoreSuggestionsView.dismissMoreKeysPanel(); + dismissMoreSuggestionsPanel(); + } + + @Override + protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) { + // Called by the framework when the size is known. Show the important notice if applicable. + // This may be overriden by showing suggestions later, if applicable. + if (oldw <= 0 && w > 0) { + maybeShowImportantNoticeTitle(); + } } } diff --git a/java/src/com/android/inputmethod/event/SoftwareKeyboardEventDecoder.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripViewAccessor.java index de91567c7..52708455e 100644 --- a/java/src/com/android/inputmethod/event/SoftwareKeyboardEventDecoder.java +++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripViewAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012 The Android Open Source Project + * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,14 +14,17 @@ * limitations under the License. */ -package com.android.inputmethod.event; +package com.android.inputmethod.latin.suggestions; + +import com.android.inputmethod.latin.SuggestedWords; /** - * A decoder for events from software keyboard, like the ones displayed by Latin IME. + * An object that gives basic control of a suggestion strip and some info on it. */ -public class SoftwareKeyboardEventDecoder implements SoftwareEventDecoder { - @Override - public Event decodeSoftwareEvent() { - return null; - } +public interface SuggestionStripViewAccessor { + public void showAddToDictionaryHint(final String word); + public boolean isShowingAddToDictionaryHint(); + public void dismissAddToDictionaryHint(); + public void setNeutralSuggestionStrip(); + public void showSuggestionStrip(final SuggestedWords suggestedWords); } diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java index 21426d1eb..eda81940f 100644 --- a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java +++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java @@ -167,7 +167,9 @@ public class UserDictionaryAddWordContents { // should not insert, because either A. the word exists with no shortcut, in which // case the exact same thing we want to insert is already there, or B. the word // exists with at least one shortcut, in which case it has priority on our word. - if (hasWord(newWord, context)) return CODE_ALREADY_PRESENT; + if (TextUtils.isEmpty(newShortcut) && hasWord(newWord, context)) { + return CODE_ALREADY_PRESENT; + } // Disallow duplicates. If the same word with no shortcut is defined, remove it; if // the same word with the same shortcut is defined, remove it; but we don't mind if @@ -256,7 +258,7 @@ public class UserDictionaryAddWordContents { // The system locale should be inside. We want it at the 2nd spot. locales.remove(systemLocale); // system locale may not be null locales.remove(""); // Remove the empty string if it's there - final ArrayList<LocaleRenderer> localesList = new ArrayList<LocaleRenderer>(); + final ArrayList<LocaleRenderer> localesList = new ArrayList<>(); // Add the passed locale, then the system locale at the top of the list. Add an // "all languages" entry at the bottom of the list. addLocaleDisplayNameToList(activity, localesList, mLocale); diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordFragment.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordFragment.java index 4fc132f68..163443036 100644 --- a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordFragment.java +++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordFragment.java @@ -134,8 +134,8 @@ public class UserDictionaryAddWordFragment extends Fragment final Spinner localeSpinner = (Spinner)mRootView.findViewById(R.id.user_dictionary_add_locale); - final ArrayAdapter<LocaleRenderer> adapter = new ArrayAdapter<LocaleRenderer>(getActivity(), - android.R.layout.simple_spinner_item, localesList); + final ArrayAdapter<LocaleRenderer> adapter = new ArrayAdapter<>( + getActivity(), android.R.layout.simple_spinner_item, localesList); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); localeSpinner.setAdapter(adapter); localeSpinner.setOnItemSelectedListener(this); diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java index 32c4950da..624783a70 100644 --- a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java +++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java @@ -53,20 +53,24 @@ public class UserDictionaryList extends PreferenceFragment { } public static TreeSet<String> getUserDictionaryLocalesSet(Activity activity) { - @SuppressWarnings("deprecation") - final Cursor cursor = activity.managedQuery(UserDictionary.Words.CONTENT_URI, + final Cursor cursor = activity.getContentResolver().query(UserDictionary.Words.CONTENT_URI, new String[] { UserDictionary.Words.LOCALE }, null, null, null); - final TreeSet<String> localeSet = new TreeSet<String>(); + final TreeSet<String> localeSet = new TreeSet<>(); if (null == cursor) { // The user dictionary service is not present or disabled. Return null. return null; - } else if (cursor.moveToFirst()) { - final int columnIndex = cursor.getColumnIndex(UserDictionary.Words.LOCALE); - do { - final String locale = cursor.getString(columnIndex); - localeSet.add(null != locale ? locale : ""); - } while (cursor.moveToNext()); + } + try { + if (cursor.moveToFirst()) { + final int columnIndex = cursor.getColumnIndex(UserDictionary.Words.LOCALE); + do { + final String locale = cursor.getString(columnIndex); + localeSet.add(null != locale ? locale : ""); + } while (cursor.moveToNext()); + } + } finally { + cursor.close(); } if (!UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED) { // For ICS, we need to show "For all languages" in case that the keyboard locale diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java index 7571e87c5..cf2014a1a 100644 --- a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java +++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java @@ -140,6 +140,11 @@ public class UserDictionarySettings extends ListFragment { } mLocale = locale; + // WARNING: The following cursor is never closed! TODO: don't put that in a member, and + // make sure all cursors are correctly closed. Also, this comes from a call to + // Activity#managedQuery, which has been deprecated for a long time (and which FORBIDS + // closing the cursor, so take care when resolving this TODO). We should either use a + // regular query and close the cursor, or switch to a LoaderManager and a CursorLoader. mCursor = createCursor(locale); TextView emptyView = (TextView) getView().findViewById(android.R.id.empty); emptyView.setText(R.string.user_dict_settings_empty_text); diff --git a/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java b/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java index d87f6f3c4..db7f2a56c 100644 --- a/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java @@ -17,53 +17,73 @@ package com.android.inputmethod.latin.utils; import static com.android.inputmethod.latin.Constants.Subtype.KEYBOARD_MODE; +import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.ASCII_CAPABLE; +import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.EMOJI_CAPABLE; import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.IS_ADDITIONAL_SUBTYPE; import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET; import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME; import android.os.Build; import android.text.TextUtils; +import android.util.Log; import android.view.inputmethod.InputMethodSubtype; +import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils; -import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.R; import java.util.ArrayList; +import java.util.Arrays; public final class AdditionalSubtypeUtils { + private static final String TAG = AdditionalSubtypeUtils.class.getSimpleName(); + private static final InputMethodSubtype[] EMPTY_SUBTYPE_ARRAY = new InputMethodSubtype[0]; private AdditionalSubtypeUtils() { // This utility class is not publicly instantiable. } + @UsedForTesting public static boolean isAdditionalSubtype(final InputMethodSubtype subtype) { return subtype.containsExtraValueKey(IS_ADDITIONAL_SUBTYPE); } private static final String LOCALE_AND_LAYOUT_SEPARATOR = ":"; + private static final int INDEX_OF_LOCALE = 0; + private static final int INDEX_OF_KEYBOARD_LAYOUT = 1; + private static final int INDEX_OF_EXTRA_VALUE = 2; + private static final int LENGTH_WITHOUT_EXTRA_VALUE = (INDEX_OF_KEYBOARD_LAYOUT + 1); + private static final int LENGTH_WITH_EXTRA_VALUE = (INDEX_OF_EXTRA_VALUE + 1); private static final String PREF_SUBTYPE_SEPARATOR = ";"; - public static InputMethodSubtype createAdditionalSubtype(final String localeString, - final String keyboardLayoutSetName, final String extraValue) { - final String layoutExtraValue = KEYBOARD_LAYOUT_SET + "=" + keyboardLayoutSetName; - final String layoutDisplayNameExtraValue; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN - && SubtypeLocaleUtils.isExceptionalLocale(localeString)) { - final String layoutDisplayName = SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName( - keyboardLayoutSetName); - layoutDisplayNameExtraValue = StringUtils.appendToCommaSplittableTextIfNotExists( - UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME + "=" + layoutDisplayName, extraValue); - } else { - layoutDisplayNameExtraValue = extraValue; - } - final String additionalSubtypeExtraValue = - StringUtils.appendToCommaSplittableTextIfNotExists( - IS_ADDITIONAL_SUBTYPE, layoutDisplayNameExtraValue); + private static InputMethodSubtype createAdditionalSubtypeInternal( + final String localeString, final String keyboardLayoutSetName, + final boolean isAsciiCapable, final boolean isEmojiCapable) { final int nameId = SubtypeLocaleUtils.getSubtypeNameId(localeString, keyboardLayoutSetName); - return buildInputMethodSubtype( - nameId, localeString, layoutExtraValue, additionalSubtypeExtraValue); + final String platformVersionDependentExtraValues = getPlatformVersionDependentExtraValue( + localeString, keyboardLayoutSetName, isAsciiCapable, isEmojiCapable); + final int platformVersionIndependentSubtypeId = + getPlatformVersionIndependentSubtypeId(localeString, keyboardLayoutSetName); + // NOTE: In KitKat and later, InputMethodSubtypeBuilder#setIsAsciiCapable is also available. + // TODO: Use InputMethodSubtypeBuilder#setIsAsciiCapable when appropriate. + return InputMethodSubtypeCompatUtils.newInputMethodSubtype(nameId, + R.drawable.ic_ime_switcher_dark, localeString, KEYBOARD_MODE, + platformVersionDependentExtraValues, + false /* isAuxiliary */, false /* overrideImplicitlyEnabledSubtype */, + platformVersionIndependentSubtypeId); + } + + public static InputMethodSubtype createDummyAdditionalSubtype( + final String localeString, final String keyboardLayoutSetName) { + return createAdditionalSubtypeInternal(localeString, keyboardLayoutSetName, + false /* isAsciiCapable */, false /* isEmojiCapable */); + } + + public static InputMethodSubtype createAsciiEmojiCapableAdditionalSubtype( + final String localeString, final String keyboardLayoutSetName) { + return createAdditionalSubtypeInternal(localeString, keyboardLayoutSetName, + true /* isAsciiCapable */, true /* isEmojiCapable */); } public static String getPrefSubtype(final InputMethodSubtype subtype) { @@ -79,26 +99,26 @@ public final class AdditionalSubtypeUtils { : basePrefSubtype + LOCALE_AND_LAYOUT_SEPARATOR + extraValue; } - public static InputMethodSubtype createAdditionalSubtype(final String prefSubtype) { - final String elems[] = prefSubtype.split(LOCALE_AND_LAYOUT_SEPARATOR); - if (elems.length < 2 || elems.length > 3) { - throw new RuntimeException("Unknown additional subtype specified: " + prefSubtype); - } - final String localeString = elems[0]; - final String keyboardLayoutSetName = elems[1]; - final String extraValue = (elems.length == 3) ? elems[2] : null; - return createAdditionalSubtype(localeString, keyboardLayoutSetName, extraValue); - } - public static InputMethodSubtype[] createAdditionalSubtypesArray(final String prefSubtypes) { if (TextUtils.isEmpty(prefSubtypes)) { return EMPTY_SUBTYPE_ARRAY; } final String[] prefSubtypeArray = prefSubtypes.split(PREF_SUBTYPE_SEPARATOR); - final ArrayList<InputMethodSubtype> subtypesList = - CollectionUtils.newArrayList(prefSubtypeArray.length); + final ArrayList<InputMethodSubtype> subtypesList = new ArrayList<>(prefSubtypeArray.length); for (final String prefSubtype : prefSubtypeArray) { - final InputMethodSubtype subtype = createAdditionalSubtype(prefSubtype); + final String elems[] = prefSubtype.split(LOCALE_AND_LAYOUT_SEPARATOR); + if (elems.length != LENGTH_WITHOUT_EXTRA_VALUE + && elems.length != LENGTH_WITH_EXTRA_VALUE) { + Log.w(TAG, "Unknown additional subtype specified: " + prefSubtype + " in " + + prefSubtypes); + continue; + } + final String localeString = elems[INDEX_OF_LOCALE]; + final String keyboardLayoutSetName = elems[INDEX_OF_KEYBOARD_LAYOUT]; + // Here we assume that all the additional subtypes have AsciiCapable and EmojiCapable. + // This is actually what the setting dialog for additional subtype is doing. + final InputMethodSubtype subtype = createAsciiEmojiCapableAdditionalSubtype( + localeString, keyboardLayoutSetName); if (subtype.getNameResId() == SubtypeLocaleUtils.UNKNOWN_KEYBOARD_LAYOUT) { // Skip unknown keyboard layout subtype. This may happen when predefined keyboard // layout has been removed. @@ -137,31 +157,81 @@ public final class AdditionalSubtypeUtils { return sb.toString(); } - private static InputMethodSubtype buildInputMethodSubtype(int nameId, String localeString, - String layoutExtraValue, String additionalSubtypeExtraValue) { - // CAVEAT! If you want to change subtypeId after changing the extra values, - // you must change "getInputMethodSubtypeId". But it will remove the additional keyboard - // from the current users. So, you should be really careful to change it. - final int subtypeId = getInputMethodSubtypeId(nameId, localeString, layoutExtraValue, - additionalSubtypeExtraValue); - final String extraValue; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - extraValue = layoutExtraValue + "," + additionalSubtypeExtraValue - + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE - + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE; - } else { - extraValue = layoutExtraValue + "," + additionalSubtypeExtraValue; + /** + * Returns the extra value that is optimized for the running OS. + * <p> + * Historically the extra value has been used as the last resort to annotate various kinds of + * attributes. Some of these attributes are valid only on some platform versions. Thus we cannot + * assume that the extra values stored in a persistent storage are always valid. We need to + * regenerate the extra value on the fly instead. + * </p> + * @param localeString the locale string (e.g., "en_US"). + * @param keyboardLayoutSetName the keyboard layout set name (e.g., "dvorak"). + * @param isAsciiCapable true when ASCII characters are supported with this layout. + * @param isEmojiCapable true when Unicode Emoji characters are supported with this layout. + * @return extra value that is optimized for the running OS. + * @see #getPlatformVersionIndependentSubtypeId(String, String) + */ + private static String getPlatformVersionDependentExtraValue(final String localeString, + final String keyboardLayoutSetName, final boolean isAsciiCapable, + final boolean isEmojiCapable) { + final ArrayList<String> extraValueItems = new ArrayList<>(); + extraValueItems.add(KEYBOARD_LAYOUT_SET + "=" + keyboardLayoutSetName); + if (isAsciiCapable) { + extraValueItems.add(ASCII_CAPABLE); } - return InputMethodSubtypeCompatUtils.newInputMethodSubtype(nameId, - R.drawable.ic_ime_switcher_dark, localeString, KEYBOARD_MODE, extraValue, - false, false, subtypeId); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && + SubtypeLocaleUtils.isExceptionalLocale(localeString)) { + extraValueItems.add(UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME + "=" + + SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(keyboardLayoutSetName)); + } + if (isEmojiCapable && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + extraValueItems.add(EMOJI_CAPABLE); + } + extraValueItems.add(IS_ADDITIONAL_SUBTYPE); + return TextUtils.join(",", extraValueItems); } - private static int getInputMethodSubtypeId(int nameId, String localeString, - String layoutExtraValue, String additionalSubtypeExtraValue) { - // TODO: Use InputMethodSubtypeBuilder once we use SDK version 19. - return (new InputMethodSubtype(nameId, R.drawable.ic_ime_switcher_dark, - localeString, KEYBOARD_MODE, layoutExtraValue + "," + additionalSubtypeExtraValue, - false, false)).hashCode(); + /** + * Returns the subtype ID that is supposed to be compatible between different version of OSes. + * <p> + * From the compatibility point of view, it is important to keep subtype id predictable and + * stable between different OSes. For this purpose, the calculation code in this method is + * carefully chosen and then fixed. Treat the following code as no more or less than a + * hash function. Each component to be hashed can be different from the corresponding value + * that is used to instantiate {@link InputMethodSubtype} actually. + * For example, you don't need to update <code>compatibilityExtraValueItems</code> in this + * method even when we need to add some new extra values for the actual instance of + * {@link InputMethodSubtype}. + * </p> + * @param localeString the locale string (e.g., "en_US"). + * @param keyboardLayoutSetName the keyboard layout set name (e.g., "dvorak"). + * @return a platform-version independent subtype ID. + * @see #getPlatformVersionDependentExtraValue(String, String, boolean, boolean) + */ + private static int getPlatformVersionIndependentSubtypeId(final String localeString, + final String keyboardLayoutSetName) { + // For compatibility reasons, we concatenate the extra values in the following order. + // - KeyboardLayoutSet + // - AsciiCapable + // - UntranslatableReplacementStringInSubtypeName + // - EmojiCapable + // - isAdditionalSubtype + final ArrayList<String> compatibilityExtraValueItems = new ArrayList<>(); + compatibilityExtraValueItems.add(KEYBOARD_LAYOUT_SET + "=" + keyboardLayoutSetName); + compatibilityExtraValueItems.add(ASCII_CAPABLE); + if (SubtypeLocaleUtils.isExceptionalLocale(localeString)) { + compatibilityExtraValueItems.add(UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME + "=" + + SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(keyboardLayoutSetName)); + } + compatibilityExtraValueItems.add(EMOJI_CAPABLE); + compatibilityExtraValueItems.add(IS_ADDITIONAL_SUBTYPE); + final String compatibilityExtraValues = TextUtils.join(",", compatibilityExtraValueItems); + return Arrays.hashCode(new Object[] { + localeString, + KEYBOARD_MODE, + compatibilityExtraValues, + false /* isAuxiliary */, + false /* overrideImplicitlyEnabledSubtype */ }); } } diff --git a/java/src/com/android/inputmethod/latin/utils/ApplicationUtils.java b/java/src/com/android/inputmethod/latin/utils/ApplicationUtils.java index 08a2a8c5a..7a4150def 100644 --- a/java/src/com/android/inputmethod/latin/utils/ApplicationUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/ApplicationUtils.java @@ -31,7 +31,7 @@ public final class ApplicationUtils { // This utility class is not publicly instantiable. } - public static int getAcitivityTitleResId(final Context context, + public static int getActivityTitleResId(final Context context, final Class<? extends Activity> cls) { final ComponentName cn = new ComponentName(context, cls); try { @@ -62,4 +62,22 @@ public final class ApplicationUtils { } return ""; } + + /** + * A utility method to get the application's PackageInfo.versionCode + * @return the application's PackageInfo.versionCode + */ + public static int getVersionCode(final Context context) { + try { + if (context == null) { + return 0; + } + final String packageName = context.getPackageName(); + final PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0); + return info.versionCode; + } catch (final NameNotFoundException e) { + Log.e(TAG, "Could not find version info.", e); + } + return 0; + } } diff --git a/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java b/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java index c2e97a36f..d12aad639 100644 --- a/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java +++ b/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java @@ -20,7 +20,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** - * This class is a holder of a result of asynchronous computation. + * This class is a holder of the result of an asynchronous computation. * * @param <E> the type of the result. */ @@ -36,9 +36,9 @@ public class AsyncResultHolder<E> { } /** - * Sets the result value to this holder. + * Sets the result value of this holder. * - * @param result the value which is set. + * @param result the value to set. */ public void set(final E result) { synchronized(mLock) { @@ -54,12 +54,12 @@ public class AsyncResultHolder<E> { * Causes the current thread to wait unless the value is set or the specified time is elapsed. * * @param defaultValue the default value. - * @param timeOut the time to wait. - * @return if the result is set until the time limit then the result, otherwise defaultValue. + * @param timeOut the maximum time to wait. + * @return if the result is set before the time limit then the result, otherwise defaultValue. */ public E get(final E defaultValue, final long timeOut) { try { - if(mLatch.await(timeOut, TimeUnit.MILLISECONDS)) { + if (mLatch.await(timeOut, TimeUnit.MILLISECONDS)) { return mResult; } else { return defaultValue; diff --git a/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java b/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java index 066c5fd32..34ee2152a 100644 --- a/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java @@ -16,16 +16,10 @@ package com.android.inputmethod.latin.utils; -import com.android.inputmethod.latin.BinaryDictionary; -import com.android.inputmethod.latin.Dictionary; -import com.android.inputmethod.latin.LatinImeLogger; -import com.android.inputmethod.latin.Suggest; -import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; - -import android.text.TextUtils; import android.util.Log; -import java.util.concurrent.ConcurrentHashMap; +import com.android.inputmethod.latin.LatinImeLogger; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; public final class AutoCorrectionUtils { private static final boolean DBG = LatinImeLogger.sDBG; @@ -36,58 +30,18 @@ public final class AutoCorrectionUtils { // Purely static class: can't instantiate. } - public static boolean isValidWord(final Suggest suggest, final String word, - final boolean ignoreCase) { - if (TextUtils.isEmpty(word)) { - return false; - } - final ConcurrentHashMap<String, Dictionary> dictionaries = suggest.getUnigramDictionaries(); - final String lowerCasedWord = word.toLowerCase(suggest.mLocale); - for (final String key : dictionaries.keySet()) { - final Dictionary dictionary = dictionaries.get(key); - // It's unclear how realistically 'dictionary' can be null, but the monkey is somehow - // managing to get null in here. Presumably the language is changing to a language with - // no main dictionary and the monkey manages to type a whole word before the thread - // that reads the dictionary is started or something? - // Ideally the passed map would come out of a {@link java.util.concurrent.Future} and - // would be immutable once it's finished initializing, but concretely a null test is - // probably good enough for the time being. - if (null == dictionary) continue; - if (dictionary.isValidWord(word) - || (ignoreCase && dictionary.isValidWord(lowerCasedWord))) { - return true; - } - } - return false; - } - - public static int getMaxFrequency(final ConcurrentHashMap<String, Dictionary> dictionaries, - final String word) { - if (TextUtils.isEmpty(word)) { - return Dictionary.NOT_A_PROBABILITY; - } - int maxFreq = -1; - for (final String key : dictionaries.keySet()) { - final Dictionary dictionary = dictionaries.get(key); - if (null == dictionary) continue; - final int tempFreq = dictionary.getFrequency(word); - if (tempFreq >= maxFreq) { - maxFreq = tempFreq; - } - } - return maxFreq; - } - public static boolean suggestionExceedsAutoCorrectionThreshold( final SuggestedWordInfo suggestion, final String consideredWord, final float autoCorrectionThreshold) { if (null != suggestion) { // Shortlist a whitelisted word - if (suggestion.mKind == SuggestedWordInfo.KIND_WHITELIST) return true; + if (suggestion.isKindOf(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( + final float normalizedScore = BinaryDictionaryUtils.calcNormalizedScore( consideredWord, suggestion.mWord, autoCorrectionSuggestionScore); if (DBG) { Log.d(TAG, "Normalized " + consideredWord + "," + suggestion + "," @@ -118,9 +72,8 @@ public final class AutoCorrectionUtils { if (typedWordLength < MINIMUM_SAFETY_NET_CHAR_LENGTH) { return false; } - final int maxEditDistanceOfNativeDictionary = - (typedWordLength < 5 ? 2 : typedWordLength / 2) + 1; - final int distance = BinaryDictionary.editDistance(typedWord, suggestion); + final int maxEditDistanceOfNativeDictionary = (typedWordLength / 2) + 1; + final int distance = BinaryDictionaryUtils.editDistance(typedWord, suggestion); if (DBG) { Log.d(TAG, "Autocorrected edit distance = " + distance + ", " + maxEditDistanceOfNativeDictionary); diff --git a/java/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtils.java b/java/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtils.java new file mode 100644 index 000000000..5d7deba15 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtils.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.utils; + +import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.BinaryDictionary; +import com.android.inputmethod.latin.makedict.DictionaryHeader; +import com.android.inputmethod.latin.makedict.UnsupportedFormatException; +import com.android.inputmethod.latin.personalization.PersonalizationHelper; + +import java.io.File; +import java.io.IOException; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class BinaryDictionaryUtils { + private static final String TAG = BinaryDictionaryUtils.class.getSimpleName(); + + private BinaryDictionaryUtils() { + // This utility class is not publicly instantiable. + } + + static { + JniUtils.loadNativeLibrary(); + } + + private static native boolean createEmptyDictFileNative(String filePath, long dictVersion, + String locale, String[] attributeKeyStringArray, String[] attributeValueStringArray); + private static native float calcNormalizedScoreNative(int[] before, int[] after, int score); + private static native int editDistanceNative(int[] before, int[] after); + private static native int setCurrentTimeForTestNative(int currentTime); + + public static DictionaryHeader getHeader(final File dictFile) + throws IOException, UnsupportedFormatException { + return getHeaderWithOffsetAndLength(dictFile, 0 /* offset */, dictFile.length()); + } + + public static DictionaryHeader getHeaderWithOffsetAndLength(final File dictFile, + final long offset, final long length) throws IOException, UnsupportedFormatException { + // dictType is never used for reading the header. Passing an empty string. + final BinaryDictionary binaryDictionary = new BinaryDictionary( + dictFile.getAbsolutePath(), offset, length, + true /* useFullEditDistance */, null /* locale */, "" /* dictType */, + false /* isUpdatable */); + final DictionaryHeader header = binaryDictionary.getHeader(); + binaryDictionary.close(); + if (header == null) { + throw new IOException(); + } + return header; + } + + public static boolean renameDict(final File dictFile, final File newDictFile) { + if (dictFile.isFile()) { + return dictFile.renameTo(newDictFile); + } else if (dictFile.isDirectory()) { + final String dictName = dictFile.getName(); + final String newDictName = newDictFile.getName(); + if (newDictFile.exists()) { + return false; + } + for (final File file : dictFile.listFiles()) { + if (!file.isFile()) { + continue; + } + final String fileName = file.getName(); + final String newFileName = fileName.replaceFirst( + Pattern.quote(dictName), Matcher.quoteReplacement(newDictName)); + if (!file.renameTo(new File(dictFile, newFileName))) { + return false; + } + } + return dictFile.renameTo(newDictFile); + } + return false; + } + + @UsedForTesting + public static boolean createEmptyDictFile(final String filePath, final long dictVersion, + final Locale locale, final Map<String, String> attributeMap) { + final String[] keyArray = new String[attributeMap.size()]; + final String[] valueArray = new String[attributeMap.size()]; + int index = 0; + for (final String key : attributeMap.keySet()) { + keyArray[index] = key; + valueArray[index] = attributeMap.get(key); + index++; + } + return createEmptyDictFileNative(filePath, dictVersion, locale.toString(), keyArray, + valueArray); + } + + public static float calcNormalizedScore(final String before, final String after, + final int score) { + return calcNormalizedScoreNative(StringUtils.toCodePointArray(before), + StringUtils.toCodePointArray(after), score); + } + + public static int editDistance(final String before, final String after) { + if (before == null || after == null) { + throw new IllegalArgumentException(); + } + return editDistanceNative(StringUtils.toCodePointArray(before), + StringUtils.toCodePointArray(after)); + } + + /** + * Control the current time to be used in the native code. If currentTime >= 0, this method sets + * the current time and gets into test mode. + * In test mode, set timestamp is used as the current time in the native code. + * If currentTime < 0, quit the test mode and returns to using time() to get the current time. + * + * @param currentTime seconds since the unix epoch + * @return current time got in the native code. + */ + @UsedForTesting + public static int setCurrentTimeForTest(final int currentTime) { + final int currentNativeTimestamp = setCurrentTimeForTestNative(currentTime); + PersonalizationHelper.currentTimeChangedForTesting(currentNativeTimestamp); + return currentNativeTimestamp; + } +} diff --git a/java/src/com/android/inputmethod/latin/utils/BoundedTreeSet.java b/java/src/com/android/inputmethod/latin/utils/BoundedTreeSet.java deleted file mode 100644 index ae1fd3f79..000000000 --- a/java/src/com/android/inputmethod/latin/utils/BoundedTreeSet.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.utils; - -import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; - -import java.util.Collection; -import java.util.Comparator; -import java.util.TreeSet; - -/** - * A TreeSet that is bounded in size and throws everything that's smaller than its limit - */ -public final class BoundedTreeSet extends TreeSet<SuggestedWordInfo> { - private final int mCapacity; - public BoundedTreeSet(final Comparator<SuggestedWordInfo> comparator, final int capacity) { - super(comparator); - mCapacity = capacity; - } - - @Override - public boolean add(final SuggestedWordInfo e) { - if (size() < mCapacity) return super.add(e); - if (comparator().compare(e, last()) > 0) return false; - super.add(e); - pollLast(); // removes the last element - return true; - } - - @Override - public boolean addAll(final Collection<? extends SuggestedWordInfo> e) { - if (null == e) return false; - return super.addAll(e); - } -} diff --git a/java/src/com/android/inputmethod/latin/utils/ByteArrayDictBuffer.java b/java/src/com/android/inputmethod/latin/utils/ByteArrayDictBuffer.java deleted file mode 100644 index 2028298f2..000000000 --- a/java/src/com/android/inputmethod/latin/utils/ByteArrayDictBuffer.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.utils; - -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer; - -/** - * This class provides an implementation for the FusionDictionary buffer interface that is backed - * by a simpled byte array. It allows to create a binary dictionary in memory. - */ -public final class ByteArrayDictBuffer implements DictBuffer { - private byte[] mBuffer; - private int mPosition; - - public ByteArrayDictBuffer(final byte[] buffer) { - mBuffer = buffer; - mPosition = 0; - } - - @Override - public int readUnsignedByte() { - return mBuffer[mPosition++] & 0xFF; - } - - @Override - public int readUnsignedShort() { - final int retval = readUnsignedByte(); - return (retval << 8) + readUnsignedByte(); - } - - @Override - public int readUnsignedInt24() { - final int retval = readUnsignedShort(); - return (retval << 8) + readUnsignedByte(); - } - - @Override - public int readInt() { - final int retval = readUnsignedShort(); - return (retval << 16) + readUnsignedShort(); - } - - @Override - public int position() { - return mPosition; - } - - @Override - public void position(int position) { - mPosition = position; - } - - @Override - public void put(final byte b) { - mBuffer[mPosition++] = b; - } - - @Override - public int limit() { - return mBuffer.length - 1; - } - - @Override - public int capacity() { - return mBuffer.length; - } -} diff --git a/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java b/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java index 3d4404a98..936219332 100644 --- a/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java @@ -21,7 +21,7 @@ import android.text.TextUtils; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.WordComposer; -import com.android.inputmethod.latin.settings.SettingsValues; +import com.android.inputmethod.latin.settings.SpacingAndPunctuations; import java.util.Locale; @@ -62,6 +62,22 @@ public final class CapsModeUtils { } /** + * Helper method to find out if a code point is starting punctuation. + * + * This include the Unicode START_PUNCTUATION category, but also some other symbols that are + * starting, like the inverted question mark or the double quote. + * + * @param codePoint the code point + * @return true if it's starting punctuation, false otherwise. + */ + private static boolean isStartPunctuation(final int codePoint) { + return (codePoint == Constants.CODE_DOUBLE_QUOTE || codePoint == Constants.CODE_SINGLE_QUOTE + || codePoint == Constants.CODE_INVERTED_QUESTION_MARK + || codePoint == Constants.CODE_INVERTED_EXCLAMATION_MARK + || Character.getType(codePoint) == Character.START_PUNCTUATION); + } + + /** * 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 @@ -74,7 +90,7 @@ public final class CapsModeUtils { * @param reqModes The modes to be checked: may be any combination of * {@link TextUtils#CAP_MODE_CHARACTERS}, {@link TextUtils#CAP_MODE_WORDS}, and * {@link TextUtils#CAP_MODE_SENTENCES}. - * @param settingsValues The current settings values. + * @param spacingAndPunctuations The current spacing and punctuations settings. * @param hasSpaceBefore Whether we should consider there is a space inserted at the end of cs * * @return Returns the actual capitalization modes that can be in effect @@ -83,7 +99,7 @@ public final class CapsModeUtils { * {@link TextUtils#CAP_MODE_SENTENCES}. */ public static int getCapsMode(final CharSequence cs, final int reqModes, - final SettingsValues settingsValues, final boolean hasSpaceBefore) { + final SpacingAndPunctuations spacingAndPunctuations, final boolean hasSpaceBefore) { // Quick description of what we want to do: // CAP_MODE_CHARACTERS is always on. // CAP_MODE_WORDS is on if there is some whitespace before the cursor. @@ -115,8 +131,7 @@ public final class CapsModeUtils { } else { for (i = cs.length(); i > 0; i--) { final char c = cs.charAt(i - 1); - if (c != Constants.CODE_DOUBLE_QUOTE && c != Constants.CODE_SINGLE_QUOTE - && Character.getType(c) != Character.START_PUNCTUATION) { + if (!isStartPunctuation(c)) { break; } } @@ -139,6 +154,20 @@ public final class CapsModeUtils { j--; } if (j <= 0 || Character.isWhitespace(prevChar)) { + if (spacingAndPunctuations.mUsesGermanRules) { + // In German typography rules, there is a specific case that the first character + // of a new line should not be capitalized if the previous line ends in a comma. + boolean hasNewLine = false; + while (--j >= 0 && Character.isWhitespace(prevChar)) { + if (Constants.CODE_ENTER == prevChar) { + hasNewLine = true; + } + prevChar = cs.charAt(j); + } + if (Constants.CODE_COMMA == prevChar && hasNewLine) { + return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes; + } + } // There are only spacing chars between the start of the paragraph and the cursor, // defined as a isWhitespace() char that is neither a isSpaceChar() nor a tab. Both // MODE_WORDS and MODE_SENTENCES should be active. @@ -167,8 +196,7 @@ public final class CapsModeUtils { // No other language has such a rule as far as I know, instead putting inside the quotation // mark as the exact thing quoted and handling the surrounding punctuation independently, // e.g. <<Did he say, "let's go home"?>> - // Hence, specifically for English, we treat this special case here. - if (Locale.ENGLISH.getLanguage().equals(settingsValues.mLocale.getLanguage())) { + if (spacingAndPunctuations.mUsesAmericanTypography) { for (; j > 0; j--) { // Here we look to go over any closing punctuation. This is because in dominant // variants of English, the final period is placed within double quotes and maybe @@ -191,17 +219,20 @@ public final class CapsModeUtils { if (c == Constants.CODE_QUESTION_MARK || c == Constants.CODE_EXCLAMATION_MARK) { return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_SENTENCES) & reqModes; } - if (settingsValues.mSentenceSeparator != c || j <= 0) { + if (!spacingAndPunctuations.isSentenceSeparator(c) || j <= 0) { return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes; } // 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,} + // looks like (\w\.){2,}. Moreover, in German, you put periods after digits for dates + // and some other things, and in German specifically we need to not go into autocaps after + // a whitespace-digits-period sequence. // To find out, we will have a simple state machine with the following states : - // START, WORD, PERIOD, ABBREVIATION + // START, WORD, PERIOD, ABBREVIATION, NUMBER // On START : (just before the first period) // letter => WORD + // digit => NUMBER if German; end with caps otherwise // 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) @@ -215,6 +246,11 @@ public final class CapsModeUtils { // letter => LETTER // period => PERIOD // otherwise => end with no caps (it was an abbreviation) + // On NUMBER : (period immediately preceded by one or more digits) + // digit => NUMBER + // letter => LETTER (promote to word) + // otherwise => end with no caps (it was a whitespace-digits-period sequence, + // or a punctuation-digits-period sequence like "11.11.") // "Not an abbreviation" in the above chart essentially covers cases like "...yes.". This // should capitalize. @@ -222,6 +258,7 @@ public final class CapsModeUtils { final int WORD = 1; final int PERIOD = 2; final int LETTER = 3; + final int NUMBER = 4; 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; @@ -234,6 +271,8 @@ public final class CapsModeUtils { state = WORD; } else if (Character.isWhitespace(c)) { return noCaps; + } else if (Character.isDigit(c) && spacingAndPunctuations.mUsesGermanRules) { + state = NUMBER; } else { return caps; } @@ -241,7 +280,7 @@ public final class CapsModeUtils { case WORD: if (Character.isLetter(c)) { state = WORD; - } else if (settingsValues.mSentenceSeparator == c) { + } else if (spacingAndPunctuations.isSentenceSeparator(c)) { state = PERIOD; } else { return caps; @@ -257,11 +296,20 @@ public final class CapsModeUtils { case LETTER: if (Character.isLetter(c)) { state = LETTER; - } else if (settingsValues.mSentenceSeparator == c) { + } else if (spacingAndPunctuations.isSentenceSeparator(c)) { state = PERIOD; } else { return noCaps; } + break; + case NUMBER: + if (Character.isLetter(c)) { + state = WORD; + } else if (Character.isDigit(c)) { + state = NUMBER; + } else { + return noCaps; + } } } // Here we arrived at the start of the line. This should behave exactly like whitespace. diff --git a/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java index cc25102ce..e3aef29ba 100644 --- a/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java @@ -16,90 +16,33 @@ package com.android.inputmethod.latin.utils; -import android.util.SparseArray; - -import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; -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.WeakHashMap; -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> WeakHashMap<K, V> newWeakHashMap() { - return new WeakHashMap<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(); + final TreeMap<K,V> treeMap = new TreeMap<>(); 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> ArrayDeque<E> newArrayDeque() { - return new ArrayDeque<E>(); - } + public static <E> ArrayList<E> arrayAsList(final E[] array, final int start, final int end) { + if (array == null) { + throw new NullPointerException(); + } + if (start < 0 || start > end || end > array.length) { + throw new IllegalArgumentException(); + } - public static <E> SparseArray<E> newSparseArray() { - return new SparseArray<E>(); + final ArrayList<E> list = new ArrayList<>(end - start); + for (int i = start; i < end; i++) { + list.add(array[i]); + } + return list; } } diff --git a/java/src/com/android/inputmethod/latin/utils/CombinedFormatUtils.java b/java/src/com/android/inputmethod/latin/utils/CombinedFormatUtils.java new file mode 100644 index 000000000..34f59e8bc --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/CombinedFormatUtils.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.inputmethod.latin.utils; + +import com.android.inputmethod.latin.makedict.DictionaryHeader; +import com.android.inputmethod.latin.makedict.ProbabilityInfo; +import com.android.inputmethod.latin.makedict.WeightedString; +import com.android.inputmethod.latin.makedict.WordProperty; + +import java.util.HashMap; + +public class CombinedFormatUtils { + public static final String DICTIONARY_TAG = "dictionary"; + public static final String BIGRAM_TAG = "bigram"; + public static final String SHORTCUT_TAG = "shortcut"; + public static final String PROBABILITY_TAG = "f"; + public static final String HISTORICAL_INFO_TAG = "historicalInfo"; + public static final String HISTORICAL_INFO_SEPARATOR = ":"; + public static final String WORD_TAG = "word"; + public static final String BEGINNING_OF_SENTENCE_TAG = "beginning_of_sentence"; + public static final String NOT_A_WORD_TAG = "not_a_word"; + public static final String BLACKLISTED_TAG = "blacklisted"; + + public static String formatAttributeMap(final HashMap<String, String> attributeMap) { + final StringBuilder builder = new StringBuilder(); + builder.append(DICTIONARY_TAG + "="); + if (attributeMap.containsKey(DictionaryHeader.DICTIONARY_ID_KEY)) { + builder.append(attributeMap.get(DictionaryHeader.DICTIONARY_ID_KEY)); + } + for (final String key : attributeMap.keySet()) { + if (key.equals(DictionaryHeader.DICTIONARY_ID_KEY)) { + continue; + } + final String value = attributeMap.get(key); + builder.append("," + key + "=" + value); + } + builder.append("\n"); + return builder.toString(); + } + + public static String formatWordProperty(final WordProperty wordProperty) { + final StringBuilder builder = new StringBuilder(); + builder.append(" " + WORD_TAG + "=" + wordProperty.mWord); + builder.append(","); + builder.append(formatProbabilityInfo(wordProperty.mProbabilityInfo)); + if (wordProperty.mIsBeginningOfSentence) { + builder.append("," + BEGINNING_OF_SENTENCE_TAG + "=true"); + } + if (wordProperty.mIsNotAWord) { + builder.append("," + NOT_A_WORD_TAG + "=true"); + } + if (wordProperty.mIsBlacklistEntry) { + builder.append("," + BLACKLISTED_TAG + "=true"); + } + builder.append("\n"); + if (wordProperty.mShortcutTargets != null) { + for (final WeightedString shortcutTarget : wordProperty.mShortcutTargets) { + builder.append(" " + SHORTCUT_TAG + "=" + shortcutTarget.mWord); + builder.append(","); + builder.append(formatProbabilityInfo(shortcutTarget.mProbabilityInfo)); + builder.append("\n"); + } + } + if (wordProperty.mBigrams != null) { + for (final WeightedString bigram : wordProperty.mBigrams) { + builder.append(" " + BIGRAM_TAG + "=" + bigram.mWord); + builder.append(","); + builder.append(formatProbabilityInfo(bigram.mProbabilityInfo)); + builder.append("\n"); + } + } + return builder.toString(); + } + + public static String formatProbabilityInfo(final ProbabilityInfo probabilityInfo) { + final StringBuilder builder = new StringBuilder(); + builder.append(PROBABILITY_TAG + "=" + probabilityInfo.mProbability); + if (probabilityInfo.hasHistoricalInfo()) { + builder.append(","); + builder.append(HISTORICAL_INFO_TAG + "="); + builder.append(probabilityInfo.mTimestamp); + builder.append(HISTORICAL_INFO_SEPARATOR); + builder.append(probabilityInfo.mLevel); + builder.append(HISTORICAL_INFO_SEPARATOR); + builder.append(probabilityInfo.mCount); + } + return builder.toString(); + } +} diff --git a/java/src/com/android/inputmethod/latin/utils/CoordinateUtils.java b/java/src/com/android/inputmethod/latin/utils/CoordinateUtils.java index 72f2cd2d9..87df013a6 100644 --- a/java/src/com/android/inputmethod/latin/utils/CoordinateUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/CoordinateUtils.java @@ -16,17 +16,19 @@ package com.android.inputmethod.latin.utils; +import java.util.Arrays; + public final class CoordinateUtils { private static final int INDEX_X = 0; private static final int INDEX_Y = 1; - private static final int ARRAY_SIZE = INDEX_Y + 1; + private static final int ELEMENT_SIZE = INDEX_Y + 1; private CoordinateUtils() { // This utility class is not publicly instantiable. } public static int[] newInstance() { - return new int[ARRAY_SIZE]; + return new int[ELEMENT_SIZE]; } public static int x(final int[] coords) { @@ -46,4 +48,44 @@ public final class CoordinateUtils { destination[INDEX_X] = source[INDEX_X]; destination[INDEX_Y] = source[INDEX_Y]; } + + public static int[] newCoordinateArray(final int arraySize) { + return new int[ELEMENT_SIZE * arraySize]; + } + + public static int[] newCoordinateArray(final int arraySize, + final int defaultX, final int defaultY) { + final int[] result = new int[ELEMENT_SIZE * arraySize]; + for (int i = 0; i < arraySize; ++i) { + setXYInArray(result, i, defaultX, defaultY); + } + return result; + } + + public static int xFromArray(final int[] coordsArray, final int index) { + return coordsArray[ELEMENT_SIZE * index + INDEX_X]; + } + + public static int yFromArray(final int[] coordsArray, final int index) { + return coordsArray[ELEMENT_SIZE * index + INDEX_Y]; + } + + public static int[] coordinateFromArray(final int[] coordsArray, final int index) { + final int baseIndex = ELEMENT_SIZE * index; + return Arrays.copyOfRange(coordsArray, baseIndex, baseIndex + ELEMENT_SIZE); + } + + public static void setXYInArray(final int[] coordsArray, final int index, + final int x, final int y) { + final int baseIndex = ELEMENT_SIZE * index; + coordsArray[baseIndex + INDEX_X] = x; + coordsArray[baseIndex + INDEX_Y] = y; + } + + public static void setCoordinateInArray(final int[] coordsArray, final int index, + final int[] coords) { + final int baseIndex = ELEMENT_SIZE * index; + coordsArray[baseIndex + INDEX_X] = coords[INDEX_X]; + coordsArray[baseIndex + INDEX_Y] = coords[INDEX_Y]; + } } diff --git a/java/src/com/android/inputmethod/latin/utils/CsvUtils.java b/java/src/com/android/inputmethod/latin/utils/CsvUtils.java index 36b927eea..a21a1373b 100644 --- a/java/src/com/android/inputmethod/latin/utils/CsvUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/CsvUtils.java @@ -17,6 +17,7 @@ package com.android.inputmethod.latin.utils; import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.Constants; import java.util.ArrayList; @@ -57,9 +58,9 @@ public final class CsvUtils { // Note that none of these characters match high or low surrogate characters, so we need not // take care of matching by code point. - private static final char COMMA = ','; - private static final char SPACE = ' '; - private static final char QUOTE = '"'; + private static final char COMMA = Constants.CODE_COMMA; + private static final char SPACE = Constants.CODE_SPACE; + private static final char QUOTE = Constants.CODE_DOUBLE_QUOTE; @SuppressWarnings("serial") public static class CsvParseException extends RuntimeException { @@ -208,7 +209,7 @@ public final class CsvUtils { @UsedForTesting public static String[] split(final int splitFlags, final String line) throws CsvParseException { final boolean trimSpaces = (splitFlags & SPLIT_FLAGS_TRIM_SPACES) != 0; - final ArrayList<String> fields = CollectionUtils.newArrayList(); + final ArrayList<String> fields = new ArrayList<>(); final int length = line.length(); int start = 0; do { diff --git a/java/src/com/android/inputmethod/latin/utils/DialogUtils.java b/java/src/com/android/inputmethod/latin/utils/DialogUtils.java new file mode 100644 index 000000000..a05c932d0 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/DialogUtils.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.utils; + +import android.content.Context; +import android.view.ContextThemeWrapper; + +import com.android.inputmethod.latin.R; + +public final class DialogUtils { + private DialogUtils() { + // This utility class is not publicly instantiable. + } + + public static Context getPlatformDialogThemeContext(final Context context) { + // Because {@link AlertDialog.Builder.create()} doesn't honor the specified theme with + // createThemeContextWrapper=false, the result dialog box has unneeded paddings around it. + return new ContextThemeWrapper(context, R.style.platformDialogTheme); + } +} diff --git a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java index 021bf0825..d76ea10c0 100644 --- a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java @@ -20,15 +20,19 @@ import android.content.ContentValues; import android.content.Context; import android.content.res.AssetManager; import android.content.res.Resources; +import android.text.TextUtils; import android.util.Log; import com.android.inputmethod.latin.AssetFileAddress; import com.android.inputmethod.latin.BinaryDictionaryGetter; +import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.makedict.BinaryDictIOUtils; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; +import com.android.inputmethod.latin.makedict.DictionaryHeader; +import com.android.inputmethod.latin.makedict.UnsupportedFormatException; +import com.android.inputmethod.latin.settings.SpacingAndPunctuations; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.Locale; @@ -278,14 +282,36 @@ public class DictionaryInfoUtils { BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR + locale.getLanguage().toString(); } - public static FileHeader getDictionaryFileHeaderOrNull(final File file) { - return BinaryDictIOUtils.getDictionaryFileHeaderOrNull(file, 0, file.length()); + public static DictionaryHeader getDictionaryFileHeaderOrNull(final File file) { + return getDictionaryFileHeaderOrNull(file, 0, file.length()); } + private static DictionaryHeader getDictionaryFileHeaderOrNull(final File file, + final long offset, final long length) { + try { + final DictionaryHeader header = + BinaryDictionaryUtils.getHeaderWithOffsetAndLength(file, offset, length); + return header; + } catch (UnsupportedFormatException e) { + return null; + } catch (IOException e) { + return null; + } + } + + /** + * Returns information of the dictionary. + * + * @param fileAddress the asset dictionary file address. + * @return information of the specified dictionary. + */ private static DictionaryInfo createDictionaryInfoFromFileAddress( final AssetFileAddress fileAddress) { - final FileHeader header = BinaryDictIOUtils.getDictionaryFileHeaderOrNull( + final DictionaryHeader header = getDictionaryFileHeaderOrNull( new File(fileAddress.mFilename), fileAddress.mOffset, fileAddress.mLength); + if (header == null) { + return null; + } final String id = header.getId(); final Locale locale = LocaleUtils.constructLocaleFromString(header.getLocaleString()); final String description = header.getDescription(); @@ -310,7 +336,7 @@ public class DictionaryInfoUtils { public static ArrayList<DictionaryInfo> getCurrentDictionaryFileNameAndVersionInfo( final Context context) { - final ArrayList<DictionaryInfo> dictList = CollectionUtils.newArrayList(); + final ArrayList<DictionaryInfo> dictList = new ArrayList<>(); // Retrieve downloaded dictionaries final File[] directoryList = getCachedDirectoryList(context); @@ -328,7 +354,7 @@ public class DictionaryInfoUtils { // Protect against cases of a less-specific dictionary being found, like an // en dictionary being used for an en_US locale. In this case, the en dictionary // should be used for en_US but discounted for listing purposes. - if (!dictionaryInfo.mLocale.equals(locale)) continue; + if (dictionaryInfo == null || !dictionaryInfo.mLocale.equals(locale)) continue; addOrUpdateDictInfo(dictList, dictionaryInfo); } } @@ -355,4 +381,32 @@ public class DictionaryInfoUtils { return dictList; } + + public static boolean looksValidForDictionaryInsertion(final CharSequence text, + final SpacingAndPunctuations spacingAndPunctuations) { + if (TextUtils.isEmpty(text)) return false; + final int length = text.length(); + // TODO: Make this test "length > Constants.DICTIONARY_MAX_WORD_LENGTH". + if (length >= Constants.DICTIONARY_MAX_WORD_LENGTH) { + return false; + } + int i = 0; + int digitCount = 0; + while (i < length) { + final int codePoint = Character.codePointAt(text, i); + final int charCount = Character.charCount(codePoint); + i += charCount; + if (Character.isDigit(codePoint)) { + // Count digits: see below + digitCount += charCount; + continue; + } + if (!spacingAndPunctuations.isWordCodePoint(codePoint)) return false; + } + // We reject strings entirely comprised of digits to avoid using PIN codes or credit + // card numbers. It would come in handy for word prediction though; a good example is + // when writing one's address where the street number is usually quite discriminative, + // as well as the postal code. + return digitCount < length; + } } diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java new file mode 100644 index 000000000..787e4a59d --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.utils; + +import java.util.List; +import java.util.Locale; + +import android.view.inputmethod.InputMethodSubtype; + +import com.android.inputmethod.latin.PrevWordsInfo; + +public interface DistracterFilter { + /** + * Determine whether a word is a distracter to words in dictionaries. + * + * @param prevWordsInfo the information of previous words. + * @param testedWord the word that will be tested to see whether it is a distracter to words + * in dictionaries. + * @param locale the locale of word. + * @return true if testedWord is a distracter, otherwise false. + */ + public boolean isDistracterToWordsInDictionaries(final PrevWordsInfo prevWordsInfo, + final String testedWord, final Locale locale); + + public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes); + + public void close(); + + public static final DistracterFilter EMPTY_DISTRACTER_FILTER = new DistracterFilter() { + @Override + public boolean isDistracterToWordsInDictionaries(PrevWordsInfo prevWordsInfo, + String testedWord, Locale locale) { + return false; + } + + @Override + public void close() { + } + + @Override + public void updateEnabledSubtypes(List<InputMethodSubtype> enabledSubtypes) { + } + }; +} diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatches.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatches.java new file mode 100644 index 000000000..0ee6236b1 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatches.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.utils; + +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +import android.content.Context; +import android.util.Log; +import android.util.LruCache; +import android.view.inputmethod.InputMethodSubtype; + +import com.android.inputmethod.latin.DictionaryFacilitator; +import com.android.inputmethod.latin.PrevWordsInfo; + +/** + * This class is used to prevent distracters being added to personalization + * or user history dictionaries + */ +public class DistracterFilterCheckingExactMatches implements DistracterFilter { + private static final String TAG = DistracterFilterCheckingExactMatches.class.getSimpleName(); + private static final boolean DEBUG = false; + + private static final long TIMEOUT_TO_WAIT_LOADING_DICTIONARIES_IN_SECONDS = 120; + private static final int MAX_DISTRACTERS_CACHE_SIZE = 512; + + private final Context mContext; + private final DictionaryFacilitator mDictionaryFacilitator; + private final LruCache<String, Boolean> mDistractersCache; + private final Object mLock = new Object(); + + /** + * Create a DistracterFilter instance. + * + * @param context the context. + */ + public DistracterFilterCheckingExactMatches(final Context context) { + mContext = context; + mDictionaryFacilitator = new DictionaryFacilitator(); + mDistractersCache = new LruCache<>(MAX_DISTRACTERS_CACHE_SIZE); + } + + @Override + public void close() { + mDictionaryFacilitator.closeDictionaries(); + } + + @Override + public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes) { + } + + private void loadDictionariesForLocale(final Locale newlocale) throws InterruptedException { + mDictionaryFacilitator.resetDictionaries(mContext, newlocale, + false /* useContactsDict */, false /* usePersonalizedDicts */, + false /* forceReloadMainDictionary */, null /* listener */); + mDictionaryFacilitator.waitForLoadingMainDictionary( + TIMEOUT_TO_WAIT_LOADING_DICTIONARIES_IN_SECONDS, TimeUnit.SECONDS); + } + + /** + * Determine whether a word is a distracter to words in dictionaries. + * + * @param prevWordsInfo the information of previous words. Not used for now. + * @param testedWord the word that will be tested to see whether it is a distracter to words + * in dictionaries. + * @param locale the locale of word. + * @return true if testedWord is a distracter, otherwise false. + */ + @Override + public boolean isDistracterToWordsInDictionaries(final PrevWordsInfo prevWordsInfo, + final String testedWord, final Locale locale) { + if (locale == null) { + return false; + } + if (!locale.equals(mDictionaryFacilitator.getLocale())) { + synchronized (mLock) { + // Reset dictionaries for the locale. + try { + mDistractersCache.evictAll(); + loadDictionariesForLocale(locale); + } catch (final InterruptedException e) { + Log.e(TAG, "Interrupted while waiting for loading dicts in DistracterFilter", + e); + return false; + } + } + } + + final Boolean isCachedDistracter = mDistractersCache.get(testedWord); + if (isCachedDistracter != null && isCachedDistracter) { + if (DEBUG) { + Log.d(TAG, "testedWord: " + testedWord); + Log.d(TAG, "isDistracter: true (cache hit)"); + } + return true; + } + // The tested word is a distracter when there is a word that is exact matched to the tested + // word and its probability is higher than the tested word's probability. + final int perfectMatchFreq = mDictionaryFacilitator.getFrequency(testedWord); + final int exactMatchFreq = mDictionaryFacilitator.getMaxFrequencyOfExactMatches(testedWord); + final boolean isDistracter = perfectMatchFreq < exactMatchFreq; + if (DEBUG) { + Log.d(TAG, "testedWord: " + testedWord); + Log.d(TAG, "perfectMatchFreq: " + perfectMatchFreq); + Log.d(TAG, "exactMatchFreq: " + exactMatchFreq); + Log.d(TAG, "isDistracter: " + isDistracter); + } + if (isDistracter) { + // Add the word to the cache. + mDistractersCache.put(testedWord, Boolean.TRUE); + } + return isDistracter; + } +} diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingIsInDictionary.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingIsInDictionary.java new file mode 100644 index 000000000..4ad4ba784 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingIsInDictionary.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.utils; + +import java.util.List; +import java.util.Locale; + +import android.view.inputmethod.InputMethodSubtype; + +import com.android.inputmethod.latin.Dictionary; +import com.android.inputmethod.latin.PrevWordsInfo; + +public class DistracterFilterCheckingIsInDictionary implements DistracterFilter { + private final DistracterFilter mDistracterFilter; + private final Dictionary mDictionary; + + public DistracterFilterCheckingIsInDictionary(final DistracterFilter distracterFilter, + final Dictionary dictionary) { + mDistracterFilter = distracterFilter; + mDictionary = dictionary; + } + + @Override + public boolean isDistracterToWordsInDictionaries(PrevWordsInfo prevWordsInfo, + String testedWord, Locale locale) { + if (mDictionary.isInDictionary(testedWord)) { + // This filter treats entries that are already in the dictionary as non-distracters + // because they have passed the filtering in the past. + return false; + } else { + return mDistracterFilter.isDistracterToWordsInDictionaries( + prevWordsInfo, testedWord, locale); + } + } + + @Override + public void updateEnabledSubtypes(List<InputMethodSubtype> enabledSubtypes) { + // Do nothing. + } + + @Override + public void close() { + // Do nothing. + } +} diff --git a/java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java b/java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java new file mode 100644 index 000000000..61da1b789 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.utils; + +import com.android.inputmethod.annotations.UsedForTesting; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +/** + * Utilities to manage executors. + */ +public class ExecutorUtils { + private static final ConcurrentHashMap<String, ExecutorService> sExecutorMap = + new ConcurrentHashMap<>(); + + private static class ThreadFactoryWithId implements ThreadFactory { + private final String mId; + + public ThreadFactoryWithId(final String id) { + mId = id; + } + + @Override + public Thread newThread(final Runnable r) { + return new Thread(r, "Executor - " + mId); + } + } + + /** + * Gets the executor for the given id. + */ + public static ExecutorService getExecutor(final String id) { + ExecutorService executor = sExecutorMap.get(id); + if (executor == null) { + synchronized(sExecutorMap) { + executor = sExecutorMap.get(id); + if (executor == null) { + executor = Executors.newSingleThreadExecutor(new ThreadFactoryWithId(id)); + sExecutorMap.put(id, executor); + } + } + } + return executor; + } + + /** + * Shutdowns all executors and removes all executors from the executor map for testing. + */ + @UsedForTesting + public static void shutdownAllExecutors() { + synchronized(sExecutorMap) { + for (final ExecutorService executor : sExecutorMap.values()) { + executor.execute(new Runnable() { + @Override + public void run() { + executor.shutdown(); + sExecutorMap.remove(executor); + } + }); + } + } + } +} diff --git a/java/src/com/android/inputmethod/latin/utils/FileUtils.java b/java/src/com/android/inputmethod/latin/utils/FileUtils.java new file mode 100644 index 000000000..f1106a6c6 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/FileUtils.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.inputmethod.latin.utils; + +import java.io.File; +import java.io.FilenameFilter; + +/** + * A simple class to help with removing directories recursively. + */ +public class FileUtils { + public static boolean deleteRecursively(final File path) { + if (path.isDirectory()) { + final File[] files = path.listFiles(); + if (files != null) { + for (final File child : files) { + deleteRecursively(child); + } + } + } + return path.delete(); + } + + public static boolean deleteFilteredFiles(final File dir, final FilenameFilter fileNameFilter) { + if (!dir.isDirectory()) { + return false; + } + final File[] files = dir.listFiles(fileNameFilter); + if (files == null) { + return false; + } + boolean hasDeletedAllFiles = true; + for (final File file : files) { + if (!deleteRecursively(file)) { + hasDeletedAllFiles = false; + } + } + return hasDeletedAllFiles; + } +} diff --git a/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java b/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java index ee2b97b2a..e300bd1d3 100644 --- a/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java @@ -26,12 +26,11 @@ import com.android.inputmethod.latin.userdictionary.UserDictionaryAddWordFragmen import com.android.inputmethod.latin.userdictionary.UserDictionaryList; import com.android.inputmethod.latin.userdictionary.UserDictionaryLocalePicker; import com.android.inputmethod.latin.userdictionary.UserDictionarySettings; -import com.android.inputmethod.research.FeedbackFragment; import java.util.HashSet; public class FragmentUtils { - private static final HashSet<String> sLatinImeFragments = new HashSet<String>(); + private static final HashSet<String> sLatinImeFragments = new HashSet<>(); static { sLatinImeFragments.add(DictionarySettingsFragment.class.getName()); sLatinImeFragments.add(AboutPreferences.class.getName()); @@ -43,7 +42,6 @@ public class FragmentUtils { sLatinImeFragments.add(UserDictionaryList.class.getName()); sLatinImeFragments.add(UserDictionaryLocalePicker.class.getName()); sLatinImeFragments.add(UserDictionarySettings.class.getName()); - sLatinImeFragments.add(FeedbackFragment.class.getName()); } public static boolean isValidFragment(String fragmentName) { diff --git a/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java b/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java new file mode 100644 index 000000000..8b7077879 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.utils; + +import android.content.Context; +import android.content.SharedPreferences; +import android.provider.Settings; +import android.provider.Settings.SettingNotFoundException; +import android.text.TextUtils; +import android.util.Log; + +import com.android.inputmethod.latin.R; + +public final class ImportantNoticeUtils { + private static final String TAG = ImportantNoticeUtils.class.getSimpleName(); + + // {@link SharedPreferences} name to save the last important notice version that has been + // displayed to users. + private static final String PREFERENCE_NAME = "important_notice_pref"; + private static final String KEY_IMPORTANT_NOTICE_VERSION = "important_notice_version"; + public static final int VERSION_TO_ENABLE_PERSONALIZED_SUGGESTIONS = 1; + + // Copy of the hidden {@link Settings.Secure#USER_SETUP_COMPLETE} settings key. + // The value is zero until each multiuser completes system setup wizard. + // Caveat: This is a hidden API. + private static final String Settings_Secure_USER_SETUP_COMPLETE = "user_setup_complete"; + private static final int USER_SETUP_IS_NOT_COMPLETE = 0; + + private ImportantNoticeUtils() { + // This utility class is not publicly instantiable. + } + + private static boolean isInSystemSetupWizard(final Context context) { + try { + final int userSetupComplete = Settings.Secure.getInt( + context.getContentResolver(), Settings_Secure_USER_SETUP_COMPLETE); + return userSetupComplete == USER_SETUP_IS_NOT_COMPLETE; + } catch (final SettingNotFoundException e) { + Log.w(TAG, "Can't find settings in Settings.Secure: key=" + + Settings_Secure_USER_SETUP_COMPLETE); + return false; + } + } + + private static SharedPreferences getImportantNoticePreferences(final Context context) { + return context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + } + + private static int getCurrentImportantNoticeVersion(final Context context) { + return context.getResources().getInteger(R.integer.config_important_notice_version); + } + + private static int getLastImportantNoticeVersion(final Context context) { + return getImportantNoticePreferences(context).getInt(KEY_IMPORTANT_NOTICE_VERSION, 0); + } + + public static int getNextImportantNoticeVersion(final Context context) { + return getLastImportantNoticeVersion(context) + 1; + } + + private static boolean hasNewImportantNotice(final Context context) { + final int lastVersion = getLastImportantNoticeVersion(context); + return getCurrentImportantNoticeVersion(context) > lastVersion; + } + + public static boolean shouldShowImportantNotice(final Context context) { + if (!hasNewImportantNotice(context)) { + return false; + } + final String importantNoticeTitle = getNextImportantNoticeTitle(context); + if (TextUtils.isEmpty(importantNoticeTitle)) { + return false; + } + if (isInSystemSetupWizard(context)) { + return false; + } + return true; + } + + public static void updateLastImportantNoticeVersion(final Context context) { + getImportantNoticePreferences(context) + .edit() + .putInt(KEY_IMPORTANT_NOTICE_VERSION, getNextImportantNoticeVersion(context)) + .apply(); + } + + public static String getNextImportantNoticeTitle(final Context context) { + final int nextVersion = getCurrentImportantNoticeVersion(context); + final String[] importantNoticeTitleArray = context.getResources().getStringArray( + R.array.important_notice_title_array); + if (nextVersion > 0 && nextVersion < importantNoticeTitleArray.length) { + return importantNoticeTitleArray[nextVersion]; + } + return null; + } + + public static String getNextImportantNoticeContents(final Context context) { + final int nextVersion = getNextImportantNoticeVersion(context); + final String[] importantNoticeContentsArray = context.getResources().getStringArray( + R.array.important_notice_contents_array); + if (nextVersion > 0 && nextVersion < importantNoticeContentsArray.length) { + return importantNoticeContentsArray[nextVersion]; + } + return null; + } +} diff --git a/java/src/com/android/inputmethod/latin/utils/JsonUtils.java b/java/src/com/android/inputmethod/latin/utils/JsonUtils.java new file mode 100644 index 000000000..6dd8d9711 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/JsonUtils.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.utils; + +import android.util.JsonReader; +import android.util.JsonWriter; +import android.util.Log; + +import java.io.Closeable; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class JsonUtils { + private static final String TAG = JsonUtils.class.getSimpleName(); + + private static final String INTEGER_CLASS_NAME = Integer.class.getSimpleName(); + private static final String STRING_CLASS_NAME = String.class.getSimpleName(); + + private static final String EMPTY_STRING = ""; + + public static List<Object> jsonStrToList(final String s) { + final ArrayList<Object> list = new ArrayList<>(); + final JsonReader reader = new JsonReader(new StringReader(s)); + try { + reader.beginArray(); + while (reader.hasNext()) { + reader.beginObject(); + while (reader.hasNext()) { + final String name = reader.nextName(); + if (name.equals(INTEGER_CLASS_NAME)) { + list.add(reader.nextInt()); + } else if (name.equals(STRING_CLASS_NAME)) { + list.add(reader.nextString()); + } else { + Log.w(TAG, "Invalid name: " + name); + reader.skipValue(); + } + } + reader.endObject(); + } + reader.endArray(); + return list; + } catch (final IOException e) { + } finally { + close(reader); + } + return Collections.<Object>emptyList(); + } + + public static String listToJsonStr(final List<Object> list) { + if (list == null || list.isEmpty()) { + return EMPTY_STRING; + } + final StringWriter sw = new StringWriter(); + final JsonWriter writer = new JsonWriter(sw); + try { + writer.beginArray(); + for (final Object o : list) { + writer.beginObject(); + if (o instanceof Integer) { + writer.name(INTEGER_CLASS_NAME).value((Integer)o); + } else if (o instanceof String) { + writer.name(STRING_CLASS_NAME).value((String)o); + } + writer.endObject(); + } + writer.endArray(); + return sw.toString(); + } catch (final IOException e) { + } finally { + close(writer); + } + return EMPTY_STRING; + } + + private static void close(final Closeable closeable) { + try { + if (closeable != null) { + closeable.close(); + } + } catch (final IOException e) { + // Ignore + } + } +} diff --git a/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java b/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java new file mode 100644 index 000000000..9ec19efa8 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.utils; + +import android.util.Log; + +import com.android.inputmethod.latin.Dictionary; +import com.android.inputmethod.latin.DictionaryFacilitator; +import com.android.inputmethod.latin.PrevWordsInfo; +import com.android.inputmethod.latin.settings.SpacingAndPunctuations; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +// Note: this class is used as a parameter type of a native method. You should be careful when you +// rename this class or field name. See BinaryDictionary#addMultipleDictionaryEntriesNative(). +public final class LanguageModelParam { + private static final String TAG = LanguageModelParam.class.getSimpleName(); + private static final boolean DEBUG = false; + private static final boolean DEBUG_TOKEN = false; + + // For now, these probability values are being referred to only when we add new entries to + // decaying dynamic binary dictionaries. When these are referred to, what matters is 0 or + // non-0. Thus, it's not meaningful to compare 10, 100, and so on. + // TODO: Revise the logic in ForgettingCurveUtils in native code. + private static final int UNIGRAM_PROBABILITY_FOR_VALID_WORD = 100; + private static final int UNIGRAM_PROBABILITY_FOR_OOV_WORD = Dictionary.NOT_A_PROBABILITY; + private static final int BIGRAM_PROBABILITY_FOR_VALID_WORD = 10; + private static final int BIGRAM_PROBABILITY_FOR_OOV_WORD = Dictionary.NOT_A_PROBABILITY; + + public final String mTargetWord; + public final int[] mWord0; + public final int[] mWord1; + // TODO: this needs to be a list of shortcuts + public final int[] mShortcutTarget; + public final int mUnigramProbability; + public final int mBigramProbability; + public final int mShortcutProbability; + public final boolean mIsNotAWord; + public final boolean mIsBlacklisted; + // Time stamp in seconds. + public final int mTimestamp; + + // Constructor for unigram. TODO: support shortcuts + public LanguageModelParam(final String word, final int unigramProbability, + final int timestamp) { + this(null /* word0 */, word, unigramProbability, Dictionary.NOT_A_PROBABILITY, timestamp); + } + + // Constructor for unigram and bigram. + public LanguageModelParam(final String word0, final String word1, + final int unigramProbability, final int bigramProbability, + final int timestamp) { + mTargetWord = word1; + mWord0 = (word0 == null) ? null : StringUtils.toCodePointArray(word0); + mWord1 = StringUtils.toCodePointArray(word1); + mShortcutTarget = null; + mUnigramProbability = unigramProbability; + mBigramProbability = bigramProbability; + mShortcutProbability = Dictionary.NOT_A_PROBABILITY; + mIsNotAWord = false; + mIsBlacklisted = false; + mTimestamp = timestamp; + } + + // Process a list of words and return a list of {@link LanguageModelParam} objects. + public static ArrayList<LanguageModelParam> createLanguageModelParamsFrom( + final List<String> tokens, final int timestamp, + final DictionaryFacilitator dictionaryFacilitator, + final SpacingAndPunctuations spacingAndPunctuations, + final DistracterFilter distracterFilter) { + final ArrayList<LanguageModelParam> languageModelParams = new ArrayList<>(); + final int N = tokens.size(); + PrevWordsInfo prevWordsInfo = PrevWordsInfo.EMPTY_PREV_WORDS_INFO; + for (int i = 0; i < N; ++i) { + final String tempWord = tokens.get(i); + if (StringUtils.isEmptyStringOrWhiteSpaces(tempWord)) { + // just skip this token + if (DEBUG_TOKEN) { + Log.d(TAG, "--- isEmptyStringOrWhiteSpaces: \"" + tempWord + "\""); + } + continue; + } + if (!DictionaryInfoUtils.looksValidForDictionaryInsertion( + tempWord, spacingAndPunctuations)) { + if (DEBUG_TOKEN) { + Log.d(TAG, "--- not looksValidForDictionaryInsertion: \"" + + tempWord + "\""); + } + // Sentence terminator found. Split. + prevWordsInfo = PrevWordsInfo.EMPTY_PREV_WORDS_INFO; + continue; + } + if (DEBUG_TOKEN) { + Log.d(TAG, "--- word: \"" + tempWord + "\""); + } + final LanguageModelParam languageModelParam = + detectWhetherVaildWordOrNotAndGetLanguageModelParam( + prevWordsInfo, tempWord, timestamp, dictionaryFacilitator, + distracterFilter); + if (languageModelParam == null) { + continue; + } + languageModelParams.add(languageModelParam); + prevWordsInfo = prevWordsInfo.getNextPrevWordsInfo( + new PrevWordsInfo.WordInfo(tempWord)); + } + return languageModelParams; + } + + private static LanguageModelParam detectWhetherVaildWordOrNotAndGetLanguageModelParam( + final PrevWordsInfo prevWordsInfo, final String targetWord, final int timestamp, + final DictionaryFacilitator dictionaryFacilitator, + final DistracterFilter distracterFilter) { + final Locale locale = dictionaryFacilitator.getLocale(); + if (locale == null) { + return null; + } + if (dictionaryFacilitator.isValidWord(targetWord, false /* ignoreCase */)) { + return createAndGetLanguageModelParamOfWord(prevWordsInfo, targetWord, timestamp, + true /* isValidWord */, locale, distracterFilter); + } + + final String lowerCaseTargetWord = targetWord.toLowerCase(locale); + if (dictionaryFacilitator.isValidWord(lowerCaseTargetWord, false /* ignoreCase */)) { + // Add the lower-cased word. + return createAndGetLanguageModelParamOfWord(prevWordsInfo, lowerCaseTargetWord, + timestamp, true /* isValidWord */, locale, distracterFilter); + } + + // Treat the word as an OOV word. + return createAndGetLanguageModelParamOfWord(prevWordsInfo, targetWord, timestamp, + false /* isValidWord */, locale, distracterFilter); + } + + private static LanguageModelParam createAndGetLanguageModelParamOfWord( + final PrevWordsInfo prevWordsInfo, final String targetWord, final int timestamp, + final boolean isValidWord, final Locale locale, + final DistracterFilter distracterFilter) { + final String word; + if (StringUtils.getCapitalizationType(targetWord) == StringUtils.CAPITALIZE_FIRST + && !prevWordsInfo.isValid() && !isValidWord) { + word = targetWord.toLowerCase(locale); + } else { + word = targetWord; + } + // Check whether the word is a distracter to words in the dictionaries. + if (distracterFilter.isDistracterToWordsInDictionaries(prevWordsInfo, word, locale)) { + if (DEBUG) { + Log.d(TAG, "The word (" + word + ") is a distracter. Skip this word."); + } + return null; + } + final int unigramProbability = isValidWord ? + UNIGRAM_PROBABILITY_FOR_VALID_WORD : UNIGRAM_PROBABILITY_FOR_OOV_WORD; + if (!prevWordsInfo.isValid()) { + if (DEBUG) { + Log.d(TAG, "--- add unigram: current(" + + (isValidWord ? "Valid" : "OOV") + ") = " + word); + } + return new LanguageModelParam(word, unigramProbability, timestamp); + } + if (DEBUG) { + Log.d(TAG, "--- add bigram: prev = " + prevWordsInfo + ", current(" + + (isValidWord ? "Valid" : "OOV") + ") = " + word); + } + final int bigramProbability = isValidWord ? + BIGRAM_PROBABILITY_FOR_VALID_WORD : BIGRAM_PROBABILITY_FOR_OOV_WORD; + return new LanguageModelParam(prevWordsInfo.mPrevWordsInfo[0].mWord, word, + unigramProbability, bigramProbability, timestamp); + } +} diff --git a/java/src/com/android/inputmethod/latin/utils/LatinImeLoggerUtils.java b/java/src/com/android/inputmethod/latin/utils/LatinImeLoggerUtils.java deleted file mode 100644 index e958a7e71..000000000 --- a/java/src/com/android/inputmethod/latin/utils/LatinImeLoggerUtils.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.utils; - -import android.text.TextUtils; - -import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.LatinImeLogger; -import com.android.inputmethod.latin.WordComposer; - -public final class LatinImeLoggerUtils { - private LatinImeLoggerUtils() { - // This utility class is not publicly instantiable. - } - - public static void onNonSeparator(final char code, final int x, final int y) { - UserLogRingCharBuffer.getInstance().push(code, x, y); - LatinImeLogger.logOnInputChar(); - } - - public static void onSeparator(final int code, final int x, final int y) { - // Helper method to log a single code point separator - // TODO: cache this mapping of a code point to a string in a sparse array in StringUtils - onSeparator(new String(new int[]{code}, 0, 1), x, y); - } - - public static void onSeparator(final String separator, final int x, final int y) { - final int length = separator.length(); - for (int i = 0; i < length; i = Character.offsetByCodePoints(separator, i, 1)) { - int codePoint = Character.codePointAt(separator, i); - // TODO: accept code points - UserLogRingCharBuffer.getInstance().push((char)codePoint, x, y); - } - LatinImeLogger.logOnInputSeparator(); - } - - public static void onAutoCorrection(final String typedWord, final String correctedWord, - final String separatorString, final WordComposer wordComposer) { - final boolean isBatchMode = wordComposer.isBatchMode(); - if (!isBatchMode && TextUtils.isEmpty(typedWord)) { - return; - } - // TODO: this fails when the separator is more than 1 code point long, but - // the backend can't handle it yet. The only case when this happens is with - // smileys and other multi-character keys. - final int codePoint = TextUtils.isEmpty(separatorString) ? Constants.NOT_A_CODE - : separatorString.codePointAt(0); - if (!isBatchMode) { - LatinImeLogger.logOnAutoCorrectionForTyping(typedWord, correctedWord, codePoint); - } else { - if (!TextUtils.isEmpty(correctedWord)) { - // We must make sure that InputPointer contains only the relative timestamps, - // not actual timestamps. - LatinImeLogger.logOnAutoCorrectionForGeometric( - "", correctedWord, codePoint, wordComposer.getInputPointers()); - } - } - } - - public static void onAutoCorrectionCancellation() { - LatinImeLogger.logOnAutoCorrectionCancelled(); - } -} diff --git a/java/src/com/android/inputmethod/latin/utils/StaticInnerHandlerWrapper.java b/java/src/com/android/inputmethod/latin/utils/LeakGuardHandlerWrapper.java index 44e5d17b4..dd6fac671 100644 --- a/java/src/com/android/inputmethod/latin/utils/StaticInnerHandlerWrapper.java +++ b/java/src/com/android/inputmethod/latin/utils/LeakGuardHandlerWrapper.java @@ -21,22 +21,22 @@ import android.os.Looper; import java.lang.ref.WeakReference; -public class StaticInnerHandlerWrapper<T> extends Handler { - private final WeakReference<T> mOuterInstanceRef; +public class LeakGuardHandlerWrapper<T> extends Handler { + private final WeakReference<T> mOwnerInstanceRef; - public StaticInnerHandlerWrapper(final T outerInstance) { - this(outerInstance, Looper.myLooper()); + public LeakGuardHandlerWrapper(final T ownerInstance) { + this(ownerInstance, Looper.myLooper()); } - public StaticInnerHandlerWrapper(final T outerInstance, final Looper looper) { + public LeakGuardHandlerWrapper(final T ownerInstance, final Looper looper) { super(looper); - if (outerInstance == null) { - throw new NullPointerException("outerInstance is null"); + if (ownerInstance == null) { + throw new NullPointerException("ownerInstance is null"); } - mOuterInstanceRef = new WeakReference<T>(outerInstance); + mOwnerInstanceRef = new WeakReference<>(ownerInstance); } - public T getOuterInstance() { - return mOuterInstanceRef.get(); + public T getOwnerInstance() { + return mOwnerInstanceRef.get(); } } diff --git a/java/src/com/android/inputmethod/latin/utils/LocaleUtils.java b/java/src/com/android/inputmethod/latin/utils/LocaleUtils.java index 22045aa38..c519a0de6 100644 --- a/java/src/com/android/inputmethod/latin/utils/LocaleUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/LocaleUtils.java @@ -30,9 +30,6 @@ import java.util.Locale; * dictionary pack. */ public final class LocaleUtils { - private static final HashMap<String, Long> EMPTY_LT_HASH_MAP = CollectionUtils.newHashMap(); - private static final String LOCALE_AND_TIME_STR_SEPARATER = ","; - private LocaleUtils() { // Intentional empty constructor for utility class. } @@ -162,18 +159,20 @@ public final class LocaleUtils { return LOCALE_MATCH <= level; } - private static final HashMap<String, Locale> sLocaleCache = CollectionUtils.newHashMap(); + private static final HashMap<String, Locale> sLocaleCache = new HashMap<>(); /** * Creates a locale from a string specification. */ public static Locale constructLocaleFromString(final String localeStr) { - if (localeStr == null) + if (localeStr == null) { return null; + } synchronized (sLocaleCache) { - if (sLocaleCache.containsKey(localeStr)) - return sLocaleCache.get(localeStr); - Locale retval = null; + Locale retval = sLocaleCache.get(localeStr); + if (retval != null) { + return retval; + } String[] localeParams = localeStr.split("_", 3); if (localeParams.length == 1) { retval = new Locale(localeParams[0]); @@ -188,38 +187,4 @@ public final class LocaleUtils { return retval; } } - - public static HashMap<String, Long> localeAndTimeStrToHashMap(String str) { - if (TextUtils.isEmpty(str)) { - return EMPTY_LT_HASH_MAP; - } - final String[] ss = str.split(LOCALE_AND_TIME_STR_SEPARATER); - final int N = ss.length; - if (N < 2 || N % 2 != 0) { - return EMPTY_LT_HASH_MAP; - } - final HashMap<String, Long> retval = CollectionUtils.newHashMap(); - for (int i = 0; i < N / 2; ++i) { - final String localeStr = ss[i * 2]; - final long time = Long.valueOf(ss[i * 2 + 1]); - retval.put(localeStr, time); - } - return retval; - } - - public static String localeAndTimeHashMapToStr(HashMap<String, Long> map) { - if (map == null || map.isEmpty()) { - return ""; - } - final StringBuilder builder = new StringBuilder(); - for (String localeStr : map.keySet()) { - if (builder.length() > 0) { - builder.append(LOCALE_AND_TIME_STR_SEPARATER); - } - final Long time = map.get(localeStr); - builder.append(localeStr).append(LOCALE_AND_TIME_STR_SEPARATER); - builder.append(String.valueOf(time)); - } - return builder.toString(); - } } diff --git a/java/src/com/android/inputmethod/latin/utils/PrevWordsInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/PrevWordsInfoUtils.java new file mode 100644 index 000000000..3cd63612c --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/PrevWordsInfoUtils.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.utils; + +import java.util.regex.Pattern; + +import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.PrevWordsInfo; +import com.android.inputmethod.latin.PrevWordsInfo.WordInfo; +import com.android.inputmethod.latin.settings.SpacingAndPunctuations; + +public final class PrevWordsInfoUtils { + private PrevWordsInfoUtils() { + // Intentional empty constructor for utility class. + } + + private static final Pattern SPACE_REGEX = Pattern.compile("\\s+"); + // Get context information from nth word before the cursor. n = 1 retrieves the words + // immediately before the cursor, n = 2 retrieves the words 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 information representing beginning-of-sentence). + // Example (when Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM is 2): + // (n = 1) "abc def|" -> abc, def + // (n = 1) "abc def |" -> abc, def + // (n = 1) "abc 'def|" -> empty, 'def + // (n = 1) "abc def. |" -> beginning-of-sentence + // (n = 1) "abc def . |" -> beginning-of-sentence + // (n = 2) "abc def|" -> beginning-of-sentence, abc + // (n = 2) "abc def |" -> beginning-of-sentence, abc + // (n = 2) "abc 'def|" -> empty. The context is different from "abc def", but we cannot + // represent this situation using PrevWordsInfo. See TODO in the method. + // TODO: The next example's result should be "abc, def". This have to be fixed before we + // retrieve the prior context of Beginning-of-Sentence. + // (n = 2) "abc def. |" -> beginning-of-sentence, abc + // (n = 2) "abc def . |" -> abc, def + // (n = 2) "abc|" -> beginning-of-sentence + // (n = 2) "abc |" -> beginning-of-sentence + // (n = 2) "abc. def|" -> beginning-of-sentence + public static PrevWordsInfo getPrevWordsInfoFromNthPreviousWord(final CharSequence prev, + final SpacingAndPunctuations spacingAndPunctuations, final int n) { + if (prev == null) return PrevWordsInfo.EMPTY_PREV_WORDS_INFO; + final String[] w = SPACE_REGEX.split(prev); + final WordInfo[] prevWordsInfo = new WordInfo[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM]; + for (int i = 0; i < prevWordsInfo.length; i++) { + final int focusedWordIndex = w.length - n - i; + // Referring to the word after the focused word. + if ((focusedWordIndex + 1) >= 0 && (focusedWordIndex + 1) < w.length) { + final String wordFollowingTheNthPrevWord = w[focusedWordIndex + 1]; + if (!wordFollowingTheNthPrevWord.isEmpty()) { + final char firstChar = wordFollowingTheNthPrevWord.charAt(0); + if (spacingAndPunctuations.isWordConnector(firstChar)) { + // The word following the focused word is starting with a word connector. + // TODO: Return meaningful context for this case. + prevWordsInfo[i] = WordInfo.EMPTY_WORD_INFO; + break; + } + } + } + // If we can't find (n + i) words, the context is beginning-of-sentence. + if (focusedWordIndex < 0) { + prevWordsInfo[i] = WordInfo.BEGINNING_OF_SENTENCE; + break; + } + final String focusedWord = w[focusedWordIndex]; + // If the word is, the context is beginning-of-sentence. + final int length = focusedWord.length(); + if (length <= 0) { + prevWordsInfo[i] = WordInfo.BEGINNING_OF_SENTENCE; + break; + } + // If ends in a sentence separator, the context is beginning-of-sentence. + final char lastChar = focusedWord.charAt(length - 1); + if (spacingAndPunctuations.isSentenceSeparator(lastChar)) { + prevWordsInfo[i] = WordInfo.BEGINNING_OF_SENTENCE; + break; + } + // If ends in a word separator or connector, the context is unclear. + // TODO: Return meaningful context for this case. + if (spacingAndPunctuations.isWordSeparator(lastChar) + || spacingAndPunctuations.isWordConnector(lastChar)) { + prevWordsInfo[i] = WordInfo.EMPTY_WORD_INFO; + break; + } + prevWordsInfo[i] = new WordInfo(focusedWord); + } + return new PrevWordsInfo(prevWordsInfo); + } +} diff --git a/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java b/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java deleted file mode 100644 index 201a70d42..000000000 --- a/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.utils; - -import java.util.Queue; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -/** - * An object that executes submitted tasks using a thread. - */ -public class PrioritizedSerialExecutor { - public static final String TAG = PrioritizedSerialExecutor.class.getSimpleName(); - - private final Object mLock = new Object(); - - private final Queue<Runnable> mTasks; - private final Queue<Runnable> mPrioritizedTasks; - private boolean mIsShutdown; - private final ThreadPoolExecutor mThreadPoolExecutor; - - // The task which is running now. - private Runnable mActive; - - public PrioritizedSerialExecutor() { - mTasks = new ConcurrentLinkedQueue<Runnable>(); - mPrioritizedTasks = new ConcurrentLinkedQueue<Runnable>(); - mIsShutdown = false; - mThreadPoolExecutor = new ThreadPoolExecutor(1 /* corePoolSize */, 1 /* maximumPoolSize */, - 0 /* keepAliveTime */, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(1)); - } - - /** - * Clears all queued tasks. - */ - public void clearAllTasks() { - synchronized(mLock) { - mTasks.clear(); - mPrioritizedTasks.clear(); - } - } - - /** - * Enqueues the given task into the task queue. - * @param r the enqueued task - */ - public void execute(final Runnable r) { - synchronized(mLock) { - if (!mIsShutdown) { - mTasks.offer(new Runnable() { - @Override - public void run() { - try { - r.run(); - } finally { - scheduleNext(); - } - } - }); - if (mActive == null) { - scheduleNext(); - } - } - } - } - - /** - * Enqueues the given task into the prioritized task queue. - * @param r the enqueued task - */ - public void executePrioritized(final Runnable r) { - synchronized(mLock) { - if (!mIsShutdown) { - mPrioritizedTasks.offer(new Runnable() { - @Override - public void run() { - try { - r.run(); - } finally { - scheduleNext(); - } - } - }); - if (mActive == null) { - scheduleNext(); - } - } - } - } - - private boolean fetchNextTasksLocked() { - mActive = mPrioritizedTasks.poll(); - if (mActive == null) { - mActive = mTasks.poll(); - } - return mActive != null; - } - - private void scheduleNext() { - synchronized(mLock) { - if (fetchNextTasksLocked()) { - mThreadPoolExecutor.execute(mActive); - } - } - } - - public void remove(final Runnable r) { - synchronized(mLock) { - mTasks.remove(r); - mPrioritizedTasks.remove(r); - } - } - - public void replaceAndExecute(final Runnable oldTask, final Runnable newTask) { - synchronized(mLock) { - if (oldTask != null) remove(oldTask); - execute(newTask); - } - } - - public void shutdown() { - synchronized(mLock) { - mIsShutdown = true; - } - } - - public boolean isTerminated() { - synchronized(mLock) { - if (!mIsShutdown) { - return false; - } - return mPrioritizedTasks.isEmpty() && mTasks.isEmpty() && mActive == null; - } - } -} diff --git a/java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java b/java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java index 0f5cd80db..e3cac97f0 100644 --- a/java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java +++ b/java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java @@ -37,12 +37,12 @@ public class RecapitalizeStatus { CAPS_MODE_ALL_UPPER }; - private static final int getStringMode(final String string, final String separators) { + private static final int getStringMode(final String string, final int[] sortedSeparators) { if (StringUtils.isIdenticalAfterUpcase(string)) { return CAPS_MODE_ALL_UPPER; } else if (StringUtils.isIdenticalAfterDowncase(string)) { return CAPS_MODE_ALL_LOWER; - } else if (StringUtils.isIdenticalAfterCapitalizeEachWord(string, separators)) { + } else if (StringUtils.isIdenticalAfterCapitalizeEachWord(string, sortedSeparators)) { return CAPS_MODE_FIRST_WORD_UPPER; } else { return CAPS_MODE_ORIGINAL_MIXED_CASE; @@ -60,26 +60,32 @@ public class RecapitalizeStatus { private int mRotationStyleCurrentIndex; private boolean mSkipOriginalMixedCaseMode; private Locale mLocale; - private String mSeparators; + private int[] mSortedSeparators; private String mStringAfter; - private boolean mIsActive; + private boolean mIsStarted; + private boolean mIsEnabled = true; + + private static final int[] EMPTY_STORTED_SEPARATORS = {}; public RecapitalizeStatus() { // By default, initialize with dummy values that won't match any real recapitalize. - initialize(-1, -1, "", Locale.getDefault(), ""); - deactivate(); + start(-1, -1, "", Locale.getDefault(), EMPTY_STORTED_SEPARATORS); + stop(); } - public void initialize(final int cursorStart, final int cursorEnd, final String string, - final Locale locale, final String separators) { + public void start(final int cursorStart, final int cursorEnd, final String string, + final Locale locale, final int[] sortedSeparators) { + if (!mIsEnabled) { + return; + } mCursorStartBefore = cursorStart; mStringBefore = string; mCursorStartAfter = cursorStart; mCursorEndAfter = cursorEnd; mStringAfter = string; - final int initialMode = getStringMode(mStringBefore, separators); + final int initialMode = getStringMode(mStringBefore, sortedSeparators); mLocale = locale; - mSeparators = separators; + mSortedSeparators = sortedSeparators; if (CAPS_MODE_ORIGINAL_MIXED_CASE == initialMode) { mRotationStyleCurrentIndex = 0; mSkipOriginalMixedCaseMode = false; @@ -94,15 +100,27 @@ public class RecapitalizeStatus { mRotationStyleCurrentIndex = currentMode; mSkipOriginalMixedCaseMode = true; } - mIsActive = true; + mIsStarted = true; + } + + public void stop() { + mIsStarted = false; + } + + public boolean isStarted() { + return mIsStarted; + } + + public void enable() { + mIsEnabled = true; } - public void deactivate() { - mIsActive = false; + public void disable() { + mIsEnabled = false; } - public boolean isActive() { - return mIsActive; + public boolean mIsEnabled() { + return mIsEnabled; } public boolean isSetAt(final int cursorStart, final int cursorEnd) { @@ -131,7 +149,7 @@ public class RecapitalizeStatus { mStringAfter = mStringBefore.toLowerCase(mLocale); break; case CAPS_MODE_FIRST_WORD_UPPER: - mStringAfter = StringUtils.capitalizeEachWord(mStringBefore, mSeparators, + mStringAfter = StringUtils.capitalizeEachWord(mStringBefore, mSortedSeparators, mLocale); break; case CAPS_MODE_ALL_UPPER: diff --git a/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java b/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java index 7c6fe93ac..64c9e2cff 100644 --- a/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java +++ b/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java @@ -34,7 +34,7 @@ public final class ResizableIntArray { throw new ArrayIndexOutOfBoundsException("length=" + mLength + "; index=" + index); } - public void add(final int index, final int val) { + public void addAt(final int index, final int val) { if (index < mLength) { mArray[index] = val; } else { diff --git a/java/src/com/android/inputmethod/latin/utils/ResourceUtils.java b/java/src/com/android/inputmethod/latin/utils/ResourceUtils.java index 22c92446a..093c5a6c1 100644 --- a/java/src/com/android/inputmethod/latin/utils/ResourceUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/ResourceUtils.java @@ -41,8 +41,7 @@ public final class ResourceUtils { // This utility class is not publicly instantiable. } - private static final HashMap<String, String> sDeviceOverrideValueMap = - CollectionUtils.newHashMap(); + private static final HashMap<String, String> sDeviceOverrideValueMap = new HashMap<>(); private static final String[] BUILD_KEYS_AND_VALUES = { "HARDWARE", Build.HARDWARE, @@ -54,8 +53,8 @@ public final class ResourceUtils { private static final String sBuildKeyValuesDebugString; static { - sBuildKeyValues = CollectionUtils.newHashMap(); - final ArrayList<String> keyValuePairs = CollectionUtils.newArrayList(); + sBuildKeyValues = new HashMap<>(); + final ArrayList<String> keyValuePairs = new ArrayList<>(); final int keyCount = BUILD_KEYS_AND_VALUES.length / 2; for (int i = 0; i < keyCount; i++) { final int index = i * 2; @@ -67,7 +66,8 @@ public final class ResourceUtils { sBuildKeyValuesDebugString = "[" + TextUtils.join(" ", keyValuePairs) + "]"; } - public static String getDeviceOverrideValue(final Resources res, final int overrideResId) { + public static String getDeviceOverrideValue(final Resources res, final int overrideResId, + final String defaultValue) { final int orientation = res.getConfiguration().orientation; final String key = overrideResId + "-" + orientation; if (sDeviceOverrideValueMap.containsKey(key)) { @@ -86,23 +86,6 @@ public final class ResourceUtils { return overrideValue; } - String defaultValue = null; - try { - defaultValue = findDefaultConstant(overrideArray); - // The defaultValue might be an empty string. - if (defaultValue == null) { - Log.w(TAG, "Couldn't find override value nor default value:" - + " resource="+ res.getResourceEntryName(overrideResId) - + " build=" + sBuildKeyValuesDebugString); - } else { - Log.i(TAG, "Found default value:" - + " resource="+ res.getResourceEntryName(overrideResId) - + " build=" + sBuildKeyValuesDebugString - + " default=" + defaultValue); - } - } catch (final DeviceOverridePatternSyntaxError e) { - Log.w(TAG, "Syntax error, ignored", e); - } sDeviceOverrideValueMap.put(key, defaultValue); return defaultValue; } @@ -152,8 +135,7 @@ public final class ResourceUtils { } final String condition = conditionConstant.substring(0, posComma); if (condition.isEmpty()) { - // Default condition. The default condition should be searched by - // {@link #findConstantForDefault(String[])}. + Log.w(TAG, "Array element has no condition: " + conditionConstant); continue; } try { @@ -199,24 +181,6 @@ public final class ResourceUtils { return matchedAll; } - @UsedForTesting - static String findDefaultConstant(final String[] conditionConstantArray) - throws DeviceOverridePatternSyntaxError { - if (conditionConstantArray == null) { - return null; - } - for (final String condition : conditionConstantArray) { - final int posComma = condition.indexOf(','); - if (posComma < 0) { - throw new DeviceOverridePatternSyntaxError("Array element has no comma", condition); - } - if (posComma == 0) { // condition is empty. - return condition.substring(posComma + 1); - } - } - return null; - } - public static int getDefaultKeyboardWidth(final Resources res) { final DisplayMetrics dm = res.getDisplayMetrics(); return dm.widthPixels; @@ -224,22 +188,23 @@ public final class ResourceUtils { public static int getDefaultKeyboardHeight(final Resources res) { final DisplayMetrics dm = res.getDisplayMetrics(); - final String keyboardHeightString = getDeviceOverrideValue(res, R.array.keyboard_heights); + final String keyboardHeightInDp = getDeviceOverrideValue( + res, R.array.keyboard_heights, null /* defaultValue */); final float keyboardHeight; - if (TextUtils.isEmpty(keyboardHeightString)) { - keyboardHeight = res.getDimension(R.dimen.keyboardHeight); + if (TextUtils.isEmpty(keyboardHeightInDp)) { + keyboardHeight = res.getDimension(R.dimen.config_default_keyboard_height); } else { - keyboardHeight = Float.parseFloat(keyboardHeightString) * dm.density; + keyboardHeight = Float.parseFloat(keyboardHeightInDp) * dm.density; } final float maxKeyboardHeight = res.getFraction( - R.fraction.maxKeyboardHeight, dm.heightPixels, dm.heightPixels); + R.fraction.config_max_keyboard_height, dm.heightPixels, dm.heightPixels); float minKeyboardHeight = res.getFraction( - R.fraction.minKeyboardHeight, dm.heightPixels, dm.heightPixels); + R.fraction.config_min_keyboard_height, dm.heightPixels, dm.heightPixels); if (minKeyboardHeight < 0.0f) { // Specified fraction was negative, so it should be calculated against display // width. minKeyboardHeight = -res.getFraction( - R.fraction.minKeyboardHeight, dm.widthPixels, dm.widthPixels); + R.fraction.config_min_keyboard_height, dm.widthPixels, dm.widthPixels); } // Keyboard height will not exceed maxKeyboardHeight and will not be less than // minKeyboardHeight. @@ -260,6 +225,10 @@ public final class ResourceUtils { return dimension >= 0; } + public static float getFloatFromFraction(final Resources res, final int fractionResId) { + return res.getFraction(fractionResId, 1, 1); + } + public static float getFraction(final TypedArray a, final int index, final float defValue) { final TypedValue value = a.peekValue(index); if (value == null || !isFractionValue(value)) { diff --git a/java/src/com/android/inputmethod/latin/utils/RunInLocale.java b/java/src/com/android/inputmethod/latin/utils/RunInLocale.java index 2c9e3b191..1ea16e6ef 100644 --- a/java/src/com/android/inputmethod/latin/utils/RunInLocale.java +++ b/java/src/com/android/inputmethod/latin/utils/RunInLocale.java @@ -30,25 +30,23 @@ public abstract class RunInLocale<T> { * Execute {@link #job(Resources)} method in specified system locale exclusively. * * @param res the resources to use. - * @param newLocale the locale to change to. + * @param newLocale the locale to change to. Run in system locale if null. * @return the value returned from {@link #job(Resources)}. */ public T runInLocale(final Resources res, final Locale newLocale) { synchronized (sLockForRunInLocale) { final Configuration conf = res.getConfiguration(); - final Locale oldLocale = conf.locale; - final boolean needsChange = (newLocale != null && !newLocale.equals(oldLocale)); + if (newLocale == null || newLocale.equals(conf.locale)) { + return job(res); + } + final Locale savedLocale = conf.locale; try { - if (needsChange) { - conf.locale = newLocale; - res.updateConfiguration(conf, null); - } + conf.locale = newLocale; + res.updateConfiguration(conf, null); return job(res); } finally { - if (needsChange) { - conf.locale = oldLocale; - res.updateConfiguration(conf, null); - } + conf.locale = savedLocale; + res.updateConfiguration(conf, null); } } } diff --git a/java/src/com/android/inputmethod/latin/utils/ScriptUtils.java b/java/src/com/android/inputmethod/latin/utils/ScriptUtils.java new file mode 100644 index 000000000..a76a6dfd7 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/ScriptUtils.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.utils; + +import java.util.Locale; +import java.util.TreeMap; + +/** + * A class to help with handling different writing scripts. + */ +public class ScriptUtils { + // Used for hardware keyboards + public static final int SCRIPT_UNKNOWN = -1; + // TODO: should we use ISO 15924 identifiers instead? + public static final int SCRIPT_LATIN = 0; + public static final int SCRIPT_CYRILLIC = 1; + public static final int SCRIPT_GREEK = 2; + public static final int SCRIPT_ARABIC = 3; + public static final int SCRIPT_HEBREW = 4; + public static final int SCRIPT_ARMENIAN = 5; + public static final int SCRIPT_GEORGIAN = 6; + public static final TreeMap<String, Integer> mSpellCheckerLanguageToScript; + static { + // List of the supported languages and their associated script. We won't check + // words written in another script than the selected script, because we know we + // don't have those in our dictionary so we will underline everything and we + // will never have any suggestions, so it makes no sense checking them, and this + // is done in {@link #shouldFilterOut}. Also, the script is used to choose which + // 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. + mSpellCheckerLanguageToScript = new TreeMap<>(); + mSpellCheckerLanguageToScript.put("cs", SCRIPT_LATIN); + mSpellCheckerLanguageToScript.put("da", SCRIPT_LATIN); + mSpellCheckerLanguageToScript.put("de", SCRIPT_LATIN); + mSpellCheckerLanguageToScript.put("el", SCRIPT_GREEK); + mSpellCheckerLanguageToScript.put("en", SCRIPT_LATIN); + mSpellCheckerLanguageToScript.put("es", SCRIPT_LATIN); + mSpellCheckerLanguageToScript.put("fi", SCRIPT_LATIN); + mSpellCheckerLanguageToScript.put("fr", SCRIPT_LATIN); + mSpellCheckerLanguageToScript.put("hr", SCRIPT_LATIN); + mSpellCheckerLanguageToScript.put("it", SCRIPT_LATIN); + mSpellCheckerLanguageToScript.put("lt", SCRIPT_LATIN); + mSpellCheckerLanguageToScript.put("lv", SCRIPT_LATIN); + mSpellCheckerLanguageToScript.put("nb", SCRIPT_LATIN); + mSpellCheckerLanguageToScript.put("nl", SCRIPT_LATIN); + mSpellCheckerLanguageToScript.put("pt", SCRIPT_LATIN); + mSpellCheckerLanguageToScript.put("sl", SCRIPT_LATIN); + mSpellCheckerLanguageToScript.put("ru", SCRIPT_CYRILLIC); + } + /* + * 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. + */ + public static boolean isLetterPartOfScript(final int codePoint, final int scriptId) { + switch (scriptId) { + 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); + case SCRIPT_GREEK: + // Greek letters are either in the 370~3FF range (Greek & Coptic), or in the + // 1F00~1FFF range (Greek extended). Our dictionary contains both sort of characters. + // Our dictionary also contains a few words with 0xF2; it would be best to check + // if that's correct, but a web search does return results for these words so + // they are probably okay. + return (codePoint >= 0x370 && codePoint <= 0x3FF) + || (codePoint >= 0x1F00 && codePoint <= 0x1FFF) + || codePoint == 0xF2; + case SCRIPT_ARABIC: + // Arabic letters can be in any of the following blocks: + // Arabic U+0600..U+06FF + // Arabic Supplement U+0750..U+077F + // Arabic Extended-A U+08A0..U+08FF + // Arabic Presentation Forms-A U+FB50..U+FDFF + // Arabic Presentation Forms-B U+FE70..U+FEFF + return (codePoint >= 0x600 && codePoint <= 0x6FF) + || (codePoint >= 0x750 && codePoint <= 0x77F) + || (codePoint >= 0x8A0 && codePoint <= 0x8FF) + || (codePoint >= 0xFB50 && codePoint <= 0xFDFF) + || (codePoint >= 0xFE70 && codePoint <= 0xFEFF); + case SCRIPT_HEBREW: + // Hebrew letters are in the Hebrew unicode block, which spans from U+0590 to U+05FF, + // or in the Alphabetic Presentation Forms block, U+FB00..U+FB4F, but only in the + // Hebrew part of that block, which is U+FB1D..U+FB4F. + return (codePoint >= 0x590 && codePoint <= 0x5FF + || codePoint >= 0xFB1D && codePoint <= 0xFB4F); + case SCRIPT_ARMENIAN: + // Armenian letters are in the Armenian unicode block, U+0530..U+058F and + // Alphabetic Presentation Forms block, U+FB00..U+FB4F, but only in the Armenian part + // of that block, which is U+FB13..U+FB17. + return (codePoint >= 0x530 && codePoint <= 0x58F + || codePoint >= 0xFB13 && codePoint <= 0xFB17); + case SCRIPT_GEORGIAN: + // Georgian letters are in the Georgian unicode block, U+10A0..U+10FF, + // or Georgian supplement block, U+2D00..U+2D2F + return (codePoint >= 0x10A0 && codePoint <= 0x10FF + || codePoint >= 0x2D00 && codePoint <= 0x2D2F); + case SCRIPT_UNKNOWN: + return true; + default: + // Should never come here + throw new RuntimeException("Impossible value of script: " + scriptId); + } + } + + public static int getScriptFromSpellCheckerLocale(final Locale locale) { + final Integer script = mSpellCheckerLanguageToScript.get(locale.getLanguage()); + if (null == script) { + throw new RuntimeException("We have been called with an unsupported language: \"" + + locale.getLanguage() + "\". Framework bug?"); + } + return script; + } +} diff --git a/java/src/com/android/inputmethod/latin/utils/SpacebarLanguageUtils.java b/java/src/com/android/inputmethod/latin/utils/SpacebarLanguageUtils.java new file mode 100644 index 000000000..1ca895fdb --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/SpacebarLanguageUtils.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.utils; + +import android.view.inputmethod.InputMethodSubtype; + +public final class SpacebarLanguageUtils { + private SpacebarLanguageUtils() { + // Intentional empty constructor for utility class. + } + + // InputMethodSubtype's display name for spacebar text in its locale. + // isAdditionalSubtype (T=true, F=false) + // locale layout | Middle Full + // ------ ------- - --------- ---------------------- + // en_US qwerty F English English (US) exception + // en_GB qwerty F English English (UK) exception + // es_US spanish F Español Español (EE.UU.) exception + // fr azerty F Français Français + // fr_CA qwerty F Français Français (Canada) + // fr_CH swiss F Français Français (Suisse) + // de qwertz F Deutsch Deutsch + // de_CH swiss T Deutsch Deutsch (Schweiz) + // zz qwerty F QWERTY QWERTY + // fr qwertz T Français Français + // de qwerty T Deutsch Deutsch + // en_US azerty T English English (US) + // zz azerty T AZERTY AZERTY + // Get InputMethodSubtype's full display name in its locale. + public static String getFullDisplayName(final InputMethodSubtype subtype) { + if (SubtypeLocaleUtils.isNoLanguage(subtype)) { + return SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(subtype); + } + return SubtypeLocaleUtils.getSubtypeLocaleDisplayName(subtype.getLocale()); + } + + // Get InputMethodSubtype's middle display name in its locale. + public static String getMiddleDisplayName(final InputMethodSubtype subtype) { + if (SubtypeLocaleUtils.isNoLanguage(subtype)) { + return SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(subtype); + } + return SubtypeLocaleUtils.getSubtypeLanguageDisplayName(subtype.getLocale()); + } +} diff --git a/java/src/com/android/inputmethod/latin/utils/SpannableStringUtils.java b/java/src/com/android/inputmethod/latin/utils/SpannableStringUtils.java index b51fd9377..38164cb36 100644 --- a/java/src/com/android/inputmethod/latin/utils/SpannableStringUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/SpannableStringUtils.java @@ -22,6 +22,7 @@ import android.text.Spanned; import android.text.SpannedString; import android.text.TextUtils; import android.text.style.SuggestionSpan; +import android.text.style.URLSpan; public final class SpannableStringUtils { /** @@ -40,12 +41,17 @@ public final class SpannableStringUtils { * are out of range in <code>dest</code>. */ public static void copyNonParagraphSuggestionSpansFrom(Spanned source, int start, int end, - Spannable dest, int destoff) { + Spannable dest, int destoff) { Object[] spans = source.getSpans(start, end, SuggestionSpan.class); for (int i = 0; i < spans.length; i++) { int fl = source.getSpanFlags(spans[i]); - if (0 != (fl & Spannable.SPAN_PARAGRAPH)) continue; + // We don't care about the PARAGRAPH flag in LatinIME code. However, if this flag + // is set, Spannable#setSpan will throw an exception unless the span is on the edge + // of a word. But the spans have been split into two by the getText{Before,After}Cursor + // methods, so after concatenation they may end in the middle of a word. + // Since we don't use them, we can just remove them and avoid crashing. + fl &= ~Spannable.SPAN_PARAGRAPH; int st = source.getSpanStart(spans[i]); int en = source.getSpanEnd(spans[i]); @@ -107,4 +113,16 @@ public final class SpannableStringUtils { return new SpannedString(ss); } + + public static boolean hasUrlSpans(final CharSequence text, + final int startIndex, final int endIndex) { + if (!(text instanceof Spanned)) { + return false; // Not spanned, so no link + } + final Spanned spanned = (Spanned)text; + // getSpans(x, y) does not return spans that start on x or end on y. x-1, y+1 does the + // trick, and works in all cases even if startIndex <= 0 or endIndex >= text.length(). + final URLSpan[] spans = spanned.getSpans(startIndex - 1, endIndex + 1, URLSpan.class); + return null != spans && spans.length > 0; + } } diff --git a/java/src/com/android/inputmethod/research/FeedbackLog.java b/java/src/com/android/inputmethod/latin/utils/StatsUtils.java index 5af194c32..79c19d077 100644 --- a/java/src/com/android/inputmethod/research/FeedbackLog.java +++ b/java/src/com/android/inputmethod/latin/utils/StatsUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 The Android Open Source Project + * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,19 +14,21 @@ * limitations under the License. */ -package com.android.inputmethod.research; +package com.android.inputmethod.latin.utils; import android.content.Context; +import com.android.inputmethod.latin.settings.SettingsValues; -import java.io.File; +public final class StatsUtils { + public static void init(final Context context) { + } + + public static void onCreate(final SettingsValues settingsValues) { + } -public class FeedbackLog extends ResearchLog { - public FeedbackLog(final File outputFile, final Context context) { - super(outputFile, context); + public static void onLoadSettings(final SettingsValues settingsValues) { } - @Override - public boolean isFeedbackLog() { - return true; + public static void onDestroy() { } } diff --git a/java/src/com/android/inputmethod/latin/utils/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java index a36548392..ceb038371 100644 --- a/java/src/com/android/inputmethod/latin/utils/StringUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/StringUtils.java @@ -16,29 +16,24 @@ package com.android.inputmethod.latin.utils; -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.settings.SettingsValues; +import static com.android.inputmethod.latin.Constants.CODE_UNSPECIFIED; import android.text.TextUtils; -import android.util.JsonReader; -import android.util.JsonWriter; -import android.util.Log; -import java.io.IOException; -import java.io.StringReader; -import java.io.StringWriter; +import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.Constants; + import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import java.util.Arrays; import java.util.Locale; public final class StringUtils { - private static final String TAG = StringUtils.class.getSimpleName(); public static final int CAPITALIZE_NONE = 0; // No caps, or mixed case public static final int CAPITALIZE_FIRST = 1; // First only public static final int CAPITALIZE_ALL = 2; // All caps + private static final String EMPTY_STRING = ""; + private StringUtils() { // This utility class is not publicly instantiable. } @@ -50,7 +45,7 @@ public final class StringUtils { public static String newSingleCodePointString(int codePoint) { if (Character.charCount(codePoint) == 1) { - // Optimization: avoid creating an temporary array for characters that are + // Optimization: avoid creating a temporary array for characters that are // represented by a single char value return String.valueOf((char) codePoint); } @@ -80,27 +75,16 @@ public final class StringUtils { return containsInArray(text, extraValues.split(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT)); } - public static String appendToCommaSplittableTextIfNotExists(final String text, - final String extraValues) { - if (TextUtils.isEmpty(extraValues)) { - return text; - } - if (containsInCommaSplittableText(text, extraValues)) { - return extraValues; - } - return extraValues + SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT + text; - } - public static String removeFromCommaSplittableTextIfExists(final String text, final String extraValues) { if (TextUtils.isEmpty(extraValues)) { - return ""; + return EMPTY_STRING; } final String[] elements = extraValues.split(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT); if (!containsInArray(text, elements)) { return extraValues; } - final ArrayList<String> result = CollectionUtils.newArrayList(elements.length - 1); + final ArrayList<String> result = new ArrayList<>(elements.length - 1); for (final String element : elements) { if (!text.equals(element)) { result.add(element); @@ -162,20 +146,87 @@ public final class StringUtils { private static final int[] EMPTY_CODEPOINTS = {}; - public static int[] toCodePointArray(final String string) { - final int length = string.length(); + public static int[] toCodePointArray(final CharSequence charSequence) { + return toCodePointArray(charSequence, 0, charSequence.length()); + } + + /** + * Converts a range of a string to an array of code points. + * @param charSequence the source string. + * @param startIndex the start index inside the string in java chars, inclusive. + * @param endIndex the end index inside the string in java chars, exclusive. + * @return a new array of code points. At most endIndex - startIndex, but possibly less. + */ + public static int[] toCodePointArray(final CharSequence charSequence, + final int startIndex, final int endIndex) { + final int length = charSequence.length(); if (length <= 0) { return EMPTY_CODEPOINTS; } - final int[] codePoints = new int[string.codePointCount(0, length)]; + final int[] codePoints = + new int[Character.codePointCount(charSequence, startIndex, endIndex)]; + copyCodePointsAndReturnCodePointCount(codePoints, charSequence, startIndex, endIndex, + false /* downCase */); + return codePoints; + } + + /** + * Copies the codepoints in a CharSequence to an int array. + * + * This method assumes there is enough space in the array to store the code points. The size + * can be measured with Character#codePointCount(CharSequence, int, int) before passing to this + * method. If the int array is too small, an ArrayIndexOutOfBoundsException will be thrown. + * Also, this method makes no effort to be thread-safe. Do not modify the CharSequence while + * this method is running, or the behavior is undefined. + * This method can optionally downcase code points before copying them, but it pays no attention + * to locale while doing so. + * + * @param destination the int array. + * @param charSequence the CharSequence. + * @param startIndex the start index inside the string in java chars, inclusive. + * @param endIndex the end index inside the string in java chars, exclusive. + * @param downCase if this is true, code points will be downcased before being copied. + * @return the number of copied code points. + */ + public static int copyCodePointsAndReturnCodePointCount(final int[] destination, + final CharSequence charSequence, final int startIndex, final int endIndex, + final boolean downCase) { int destIndex = 0; - for (int index = 0; index < length; index = string.offsetByCodePoints(index, 1)) { - codePoints[destIndex] = string.codePointAt(index); + for (int index = startIndex; index < endIndex; + index = Character.offsetByCodePoints(charSequence, index, 1)) { + final int codePoint = Character.codePointAt(charSequence, index); + // TODO: stop using this, as it's not aware of the locale and does not always do + // the right thing. + destination[destIndex] = downCase ? Character.toLowerCase(codePoint) : codePoint; destIndex++; } + return destIndex; + } + + public static int[] toSortedCodePointArray(final String string) { + final int[] codePoints = toCodePointArray(string); + Arrays.sort(codePoints); return codePoints; } + /** + * Construct a String from a code point array + * + * @param codePoints a code point array that is null terminated when its logical length is + * shorter than the array length. + * @return a string constructed from the code point array. + */ + public static String getStringFromNullTerminatedCodePointArray(final int[] codePoints) { + int stringLength = codePoints.length; + for (int i = 0; i < codePoints.length; i++) { + if (codePoints[i] == 0) { + stringLength = i; + break; + } + } + return new String(codePoints, 0 /* offset */, stringLength); + } + // This method assumes the text is not null. For the empty string, it returns CAPITALIZE_NONE. public static int getCapitalizationType(final String text) { // If the first char is not uppercase, then the word is either all lower case or @@ -239,65 +290,40 @@ public final class StringUtils { return true; } - @UsedForTesting - public static boolean looksValidForDictionaryInsertion(final CharSequence text, - final SettingsValues settings) { - if (TextUtils.isEmpty(text)) return false; - final int length = text.length(); - int i = 0; - int digitCount = 0; - while (i < length) { - final int codePoint = Character.codePointAt(text, i); - final int charCount = Character.charCount(codePoint); - i += charCount; - if (Character.isDigit(codePoint)) { - // Count digits: see below - digitCount += charCount; - continue; - } - if (!settings.isWordCodePoint(codePoint)) return false; - } - // We reject strings entirely comprised of digits to avoid using PIN codes or credit - // card numbers. It would come in handy for word prediction though; a good example is - // when writing one's address where the street number is usually quite discriminative, - // as well as the postal code. - return digitCount < length; - } - public static boolean isIdenticalAfterCapitalizeEachWord(final String text, - final String separators) { - boolean needCapsNext = true; + final int[] sortedSeparators) { + boolean needsCapsNext = true; final int len = text.length(); for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) { final int codePoint = text.codePointAt(i); if (Character.isLetter(codePoint)) { - if ((needCapsNext && !Character.isUpperCase(codePoint)) - || (!needCapsNext && !Character.isLowerCase(codePoint))) { + if ((needsCapsNext && !Character.isUpperCase(codePoint)) + || (!needsCapsNext && !Character.isLowerCase(codePoint))) { return false; } } // We need a capital letter next if this is a separator. - needCapsNext = (-1 != separators.indexOf(codePoint)); + needsCapsNext = (Arrays.binarySearch(sortedSeparators, codePoint) >= 0); } return true; } // TODO: like capitalizeFirst*, this does not work perfectly for Dutch because of the IJ digraph // which should be capitalized together in *some* cases. - public static String capitalizeEachWord(final String text, final String separators, + public static String capitalizeEachWord(final String text, final int[] sortedSeparators, final Locale locale) { final StringBuilder builder = new StringBuilder(); - boolean needCapsNext = true; + boolean needsCapsNext = true; final int len = text.length(); for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) { final String nextChar = text.substring(i, text.offsetByCodePoints(i, 1)); - if (needCapsNext) { + if (needsCapsNext) { builder.append(nextChar.toUpperCase(locale)); } else { builder.append(nextChar.toLowerCase(locale)); } // We need a capital letter next if this is a separator. - needCapsNext = (-1 != separators.indexOf(nextChar.codePointAt(0))); + needsCapsNext = (Arrays.binarySearch(sortedSeparators, nextChar.codePointAt(0)) >= 0); } return builder.toString(); } @@ -328,7 +354,7 @@ public final class StringUtils { boolean hasPeriod = false; int codePoint = 0; while (i > 0) { - codePoint = Character.codePointBefore(text, i); + codePoint = Character.codePointBefore(text, i); if (codePoint < Constants.CODE_PERIOD || codePoint > 'z') { // Handwavy heuristic to see if that's a URL character. Anything between period // and z. This includes all lower- and upper-case ascii letters, period, @@ -367,7 +393,49 @@ public final class StringUtils { return false; } - public static boolean isEmptyStringOrWhiteSpaces(String s) { + /** + * Examines the string and returns whether we're inside a double quote. + * + * This is used to decide whether we should put an automatic space before or after a double + * quote character. If we're inside a quotation, then we want to close it, so we want a space + * after and not before. Otherwise, we want to open the quotation, so we want a space before + * and not after. Exception: after a digit, we never want a space because the "inch" or + * "minutes" use cases is dominant after digits. + * In the practice, we determine whether we are in a quotation or not by finding the previous + * double quote character, and looking at whether it's followed by whitespace. If so, that + * was a closing quotation mark, so we're not inside a double quote. If it's not followed + * by whitespace, then it was an opening quotation mark, and we're inside a quotation. + * + * @param text the text to examine. + * @return whether we're inside a double quote. + */ + public static boolean isInsideDoubleQuoteOrAfterDigit(final CharSequence text) { + int i = text.length(); + if (0 == i) return false; + int codePoint = Character.codePointBefore(text, i); + if (Character.isDigit(codePoint)) return true; + int prevCodePoint = 0; + while (i > 0) { + codePoint = Character.codePointBefore(text, i); + if (Constants.CODE_DOUBLE_QUOTE == codePoint) { + // If we see a double quote followed by whitespace, then that + // was a closing quote. + if (Character.isWhitespace(prevCodePoint)) return false; + } + if (Character.isWhitespace(codePoint) && Constants.CODE_DOUBLE_QUOTE == prevCodePoint) { + // If we see a double quote preceded by whitespace, then that + // was an opening quote. No need to continue seeking. + return true; + } + i -= Character.charCount(codePoint); + prevCodePoint = codePoint; + } + // We reached the start of text. If the first char is a double quote, then we're inside + // a double quote. Otherwise we're not. + return Constants.CODE_DOUBLE_QUOTE == codePoint; + } + + public static boolean isEmptyStringOrWhiteSpaces(final String s) { final int N = codePointCount(s); for (int i = 0; i < N; ++i) { if (!Character.isWhitespace(s.codePointAt(i))) { @@ -378,9 +446,9 @@ public final class StringUtils { } @UsedForTesting - public static String byteArrayToHexString(byte[] bytes) { + public static String byteArrayToHexString(final byte[] bytes) { if (bytes == null || bytes.length == 0) { - return ""; + return EMPTY_STRING; } final StringBuilder sb = new StringBuilder(); for (byte b : bytes) { @@ -393,7 +461,7 @@ public final class StringUtils { * Convert hex string to byte array. The string length must be an even number. */ @UsedForTesting - public static byte[] hexStringToByteArray(String hexString) { + public static byte[] hexStringToByteArray(final String hexString) { if (TextUtils.isEmpty(hexString)) { return null; } @@ -410,66 +478,68 @@ public final class StringUtils { return bytes; } - public static List<Object> jsonStrToList(String s) { - final ArrayList<Object> retval = CollectionUtils.newArrayList(); - final JsonReader reader = new JsonReader(new StringReader(s)); - try { - reader.beginArray(); - while(reader.hasNext()) { - reader.beginObject(); - while (reader.hasNext()) { - final String name = reader.nextName(); - if (name.equals(Integer.class.getSimpleName())) { - retval.add(reader.nextInt()); - } else if (name.equals(String.class.getSimpleName())) { - retval.add(reader.nextString()); - } else { - Log.w(TAG, "Invalid name: " + name); - reader.skipValue(); - } - } - reader.endObject(); - } - reader.endArray(); - return retval; - } catch (IOException e) { - } finally { - try { - reader.close(); - } catch (IOException e) { - } + public static String toUpperCaseOfStringForLocale(final String text, + final boolean needsToUpperCase, final Locale locale) { + if (text == null || !needsToUpperCase) return text; + return text.toUpperCase(locale); + } + + public static int toUpperCaseOfCodeForLocale(final int code, final boolean needsToUpperCase, + final Locale locale) { + if (!Constants.isLetterCode(code) || !needsToUpperCase) return code; + final String text = newSingleCodePointString(code); + final String casedText = toUpperCaseOfStringForLocale( + text, needsToUpperCase, locale); + return codePointCount(casedText) == 1 + ? casedText.codePointAt(0) : CODE_UNSPECIFIED; + } + + public static int getTrailingSingleQuotesCount(final CharSequence charSequence) { + final int lastIndex = charSequence.length() - 1; + int i = lastIndex; + while (i >= 0 && charSequence.charAt(i) == Constants.CODE_SINGLE_QUOTE) { + --i; } - return Collections.<Object>emptyList(); + return lastIndex - i; } - public static String listToJsonStr(List<Object> list) { - if (list == null || list.isEmpty()) { - return ""; - } - final StringWriter sw = new StringWriter(); - final JsonWriter writer = new JsonWriter(sw); - try { - writer.beginArray(); - for (final Object o : list) { - writer.beginObject(); - if (o instanceof Integer) { - writer.name(Integer.class.getSimpleName()).value((Integer)o); - } else if (o instanceof String) { - writer.name(String.class.getSimpleName()).value((String)o); - } - writer.endObject(); + @UsedForTesting + public static class Stringizer<E> { + public String stringize(final E element) { + return element != null ? element.toString() : "null"; + } + + @UsedForTesting + public final String join(final E[] array) { + return joinStringArray(toStringArray(array), null /* delimiter */); + } + + @UsedForTesting + public final String join(final E[] array, final String delimiter) { + return joinStringArray(toStringArray(array), delimiter); + } + + protected String[] toStringArray(final E[] array) { + final String[] stringArray = new String[array.length]; + for (int index = 0; index < array.length; index++) { + stringArray[index] = stringize(array[index]); } - writer.endArray(); - return sw.toString(); - } catch (IOException e) { - } finally { - try { - if (writer != null) { - writer.close(); - } - } catch (IOException e) { + return stringArray; + } + + protected String joinStringArray(final String[] stringArray, final String delimiter) { + if (stringArray == null) { + return "null"; + } + if (delimiter == null) { + return Arrays.toString(stringArray); + } + final StringBuilder sb = new StringBuilder(); + for (int index = 0; index < stringArray.length; index++) { + sb.append(index == 0 ? "[" : delimiter); + sb.append(stringArray[index]); } + return sb + "]"; } - return ""; } } diff --git a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java index 102a41b4e..351d01400 100644 --- a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java @@ -25,17 +25,18 @@ import android.os.Build; import android.util.Log; import android.view.inputmethod.InputMethodSubtype; -import com.android.inputmethod.latin.DictionaryFactory; +import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.R; +import java.util.Arrays; import java.util.HashMap; import java.util.Locale; public final class SubtypeLocaleUtils { - static final String TAG = SubtypeLocaleUtils.class.getSimpleName(); - // This class must be located in the same package as LatinIME.java. - private static final String RESOURCE_PACKAGE_NAME = - DictionaryFactory.class.getPackage().getName(); + private static final String TAG = SubtypeLocaleUtils.class.getSimpleName(); + + // This reference class {@link Constants} must be located in the same package as LatinIME.java. + private static final String RESOURCE_PACKAGE_NAME = Constants.class.getPackage().getName(); // Special language code to represent "no language". public static final String NO_LANGUAGE = "zz"; @@ -43,21 +44,19 @@ public final class SubtypeLocaleUtils { public static final String EMOJI = "emoji"; public static final int UNKNOWN_KEYBOARD_LAYOUT = R.string.subtype_generic; - private static boolean sInitialized = false; + private static volatile boolean sInitialized = false; + private static final Object sInitializeLock = new Object(); private static Resources sResources; private static String[] sPredefinedKeyboardLayoutSet; // Keyboard layout to its display name map. - private static final HashMap<String, String> sKeyboardLayoutToDisplayNameMap = - CollectionUtils.newHashMap(); + private static final HashMap<String, String> sKeyboardLayoutToDisplayNameMap = new HashMap<>(); // Keyboard layout to subtype name resource id map. - private static final HashMap<String, Integer> sKeyboardLayoutToNameIdsMap = - CollectionUtils.newHashMap(); + private static final HashMap<String, Integer> sKeyboardLayoutToNameIdsMap = new HashMap<>(); // Exceptional locale to subtype name resource id map. - private static final HashMap<String, Integer> sExceptionalLocaleToNameIdsMap = - CollectionUtils.newHashMap(); + private static final HashMap<String, Integer> sExceptionalLocaleToNameIdsMap = new HashMap<>(); // Exceptional locale to subtype name with layout resource id map. private static final HashMap<String, Integer> sExceptionalLocaleToWithLayoutNameIdsMap = - CollectionUtils.newHashMap(); + new HashMap<>(); private static final String SUBTYPE_NAME_RESOURCE_PREFIX = "string/subtype_"; private static final String SUBTYPE_NAME_RESOURCE_GENERIC_PREFIX = @@ -69,16 +68,23 @@ public final class SubtypeLocaleUtils { // 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 = - CollectionUtils.newHashMap(); + new HashMap<>(); private SubtypeLocaleUtils() { // Intentional empty constructor for utility class. } // Note that this initialization method can be called multiple times. - public static synchronized void init(final Context context) { - if (sInitialized) return; + public static void init(final Context context) { + synchronized (sInitializeLock) { + if (sInitialized == false) { + initLocked(context); + sInitialized = true; + } + } + } + private static void initLocked(final Context context) { final Resources res = context.getResources(); sResources = res; @@ -121,8 +127,6 @@ public final class SubtypeLocaleUtils { final String keyboardLayoutSet = keyboardLayoutSetMap[i + 1]; sLocaleAndExtraValueToKeyboardLayoutSetMap.put(key, keyboardLayoutSet); } - - sInitialized = true; } public static String[] getPredefinedKeyboardLayoutSet() { @@ -166,8 +170,18 @@ public final class SubtypeLocaleUtils { return getSubtypeLocaleDisplayNameInternal(localeString, displayLocale); } + public static String getSubtypeLanguageDisplayName(final String localeString) { + final Locale locale = LocaleUtils.constructLocaleFromString(localeString); + final Locale displayLocale = getDisplayLocaleOfSubtypeLocale(localeString); + return getSubtypeLocaleDisplayNameInternal(locale.getLanguage(), displayLocale); + } + private static String getSubtypeLocaleDisplayNameInternal(final String localeString, final Locale displayLocale) { + if (NO_LANGUAGE.equals(localeString)) { + // No language subtype should be displayed in system locale. + return sResources.getString(R.string.subtype_no_language); + } final Integer exceptionalNameResId = sExceptionalLocaleToNameIdsMap.get(localeString); final String displayName; if (exceptionalNameResId != null) { @@ -178,9 +192,6 @@ public final class SubtypeLocaleUtils { } }; displayName = getExceptionalName.runInLocale(sResources, displayLocale); - } else if (NO_LANGUAGE.equals(localeString)) { - // No language subtype should be displayed in system locale. - return sResources.getString(R.string.subtype_no_language); } else { final Locale locale = LocaleUtils.constructLocaleFromString(localeString); displayName = locale.getDisplayName(displayLocale); @@ -197,12 +208,14 @@ public final class SubtypeLocaleUtils { // es_US spanish F Español (EE.UU.) exception // fr azerty F Français // fr_CA qwerty F Français (Canada) + // fr_CH swiss F Français (Suisse) // de qwertz F Deutsch - // zz qwerty F No language (QWERTY) in system locale + // de_CH swiss T Deutsch (Schweiz) + // zz qwerty F Alphabet (QWERTY) in system locale // fr qwertz T Français (QWERTZ) // de qwerty T Deutsch (QWERTY) // en_US azerty T English (US) (AZERTY) exception - // zz azerty T No language (AZERTY) in system locale + // zz azerty T Alphabet (AZERTY) in system locale private static String getReplacementString(final InputMethodSubtype subtype, final Locale displayLocale) { @@ -289,45 +302,27 @@ public final class SubtypeLocaleUtils { return keyboardLayoutSet; } - // InputMethodSubtype's display name for spacebar text in its locale. - // isAdditionalSubtype (T=true, F=false) - // locale layout | Short Middle Full - // ------ ------- - ---- --------- ---------------------- - // en_US qwerty F En English English (US) exception - // en_GB qwerty F En English English (UK) exception - // es_US spanish F Es Español Español (EE.UU.) exception - // fr azerty F Fr Français Français - // fr_CA qwerty F Fr Français Français (Canada) - // de qwertz F De Deutsch Deutsch - // zz qwerty F QWERTY QWERTY - // fr qwertz T Fr Français Français - // de qwerty T De Deutsch Deutsch - // en_US azerty T En English English (US) - // zz azerty T AZERTY AZERTY - - // Get InputMethodSubtype's full display name in its locale. - public static String getFullDisplayName(final InputMethodSubtype subtype) { - if (isNoLanguage(subtype)) { - return getKeyboardLayoutSetDisplayName(subtype); - } - return getSubtypeLocaleDisplayName(subtype.getLocale()); + // TODO: Get this information from the framework instead of maintaining here by ourselves. + // Sorted list of known Right-To-Left language codes. + private static final String[] SORTED_RTL_LANGUAGES = { + "ar", // Arabic + "fa", // Persian + "iw", // Hebrew + }; + static { + Arrays.sort(SORTED_RTL_LANGUAGES); } - // Get InputMethodSubtype's middle display name in its locale. - public static String getMiddleDisplayName(final InputMethodSubtype subtype) { - if (isNoLanguage(subtype)) { - return getKeyboardLayoutSetDisplayName(subtype); - } - final Locale locale = getSubtypeLocale(subtype); - return getSubtypeLocaleDisplayName(locale.getLanguage()); + public static boolean isRtlLanguage(final Locale locale) { + final String language = locale.getLanguage(); + return Arrays.binarySearch(SORTED_RTL_LANGUAGES, language) >= 0; } - // Get InputMethodSubtype's short display name in its locale. - public static String getShortDisplayName(final InputMethodSubtype subtype) { - if (isNoLanguage(subtype)) { - return ""; - } - final Locale locale = getSubtypeLocale(subtype); - return StringUtils.capitalizeFirstCodePoint(locale.getLanguage(), locale); + public static boolean isRtlLanguage(final InputMethodSubtype subtype) { + return isRtlLanguage(getSubtypeLocale(subtype)); + } + + public static String getCombiningRulesExtraValue(final InputMethodSubtype subtype) { + return subtype.getExtraValueOf(Constants.Subtype.ExtraValue.COMBINING_RULES); } } diff --git a/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java b/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java new file mode 100644 index 000000000..5c109a68c --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.utils; + +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import com.android.inputmethod.latin.define.ProductionFlag; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.Locale; +import java.util.TreeSet; + +/** + * A TreeSet of SuggestedWordInfo that is bounded in size and throws everything that's smaller + * than its limit + */ +public final class SuggestionResults extends TreeSet<SuggestedWordInfo> { + public final Locale mLocale; + public final ArrayList<SuggestedWordInfo> mRawSuggestions; + private final int mCapacity; + + public SuggestionResults(final Locale locale, final int capacity) { + this(locale, sSuggestedWordInfoComparator, capacity); + } + + public SuggestionResults(final Locale locale, final Comparator<SuggestedWordInfo> comparator, + final int capacity) { + super(comparator); + mLocale = locale; + mCapacity = capacity; + if (ProductionFlag.INCLUDE_RAW_SUGGESTIONS) { + mRawSuggestions = new ArrayList<>(); + } else { + mRawSuggestions = null; + } + } + + @Override + public boolean add(final SuggestedWordInfo e) { + if (size() < mCapacity) return super.add(e); + if (comparator().compare(e, last()) > 0) return false; + super.add(e); + pollLast(); // removes the last element + return true; + } + + @Override + public boolean addAll(final Collection<? extends SuggestedWordInfo> e) { + if (null == e) return false; + return super.addAll(e); + } + + private static final class SuggestedWordInfoComparator + implements Comparator<SuggestedWordInfo> { + // This comparator ranks the word info with the higher frequency first. That's because + // that's the order we want our elements in. + @Override + public int compare(final SuggestedWordInfo o1, final SuggestedWordInfo o2) { + if (o1.mScore > o2.mScore) return -1; + if (o1.mScore < o2.mScore) return 1; + if (o1.mCodePointCount < o2.mCodePointCount) return -1; + if (o1.mCodePointCount > o2.mCodePointCount) return 1; + return o1.mWord.compareTo(o2.mWord); + } + } + + private static final SuggestedWordInfoComparator sSuggestedWordInfoComparator = + new SuggestedWordInfoComparator(); +} diff --git a/java/src/com/android/inputmethod/latin/utils/TargetPackageInfoGetterTask.java b/java/src/com/android/inputmethod/latin/utils/TargetPackageInfoGetterTask.java index afbe2ecad..ab2b00e36 100644 --- a/java/src/com/android/inputmethod/latin/utils/TargetPackageInfoGetterTask.java +++ b/java/src/com/android/inputmethod/latin/utils/TargetPackageInfoGetterTask.java @@ -22,11 +22,12 @@ import android.content.pm.PackageManager; import android.os.AsyncTask; import android.util.LruCache; +import com.android.inputmethod.compat.AppWorkaroundsUtils; + public final class TargetPackageInfoGetterTask extends AsyncTask<String, Void, PackageInfo> { private static final int MAX_CACHE_ENTRIES = 64; // arbitrary - private static final LruCache<String, PackageInfo> sCache = - new LruCache<String, PackageInfo>(MAX_CACHE_ENTRIES); + private static final LruCache<String, PackageInfo> sCache = new LruCache<>(MAX_CACHE_ENTRIES); public static PackageInfo getCachedPackageInfo(final String packageName) { if (null == packageName) return null; @@ -37,17 +38,13 @@ public final class TargetPackageInfoGetterTask extends sCache.remove(packageName); } - public interface OnTargetPackageInfoKnownListener { - public void onTargetPackageInfoKnown(final PackageInfo info); - } - private Context mContext; - private final OnTargetPackageInfoKnownListener mListener; + private final AsyncResultHolder<AppWorkaroundsUtils> mResult; public TargetPackageInfoGetterTask(final Context context, - final OnTargetPackageInfoKnownListener listener) { + final AsyncResultHolder<AppWorkaroundsUtils> result) { mContext = context; - mListener = listener; + mResult = result; } @Override @@ -65,6 +62,6 @@ public final class TargetPackageInfoGetterTask extends @Override protected void onPostExecute(final PackageInfo info) { - mListener.onTargetPackageInfoKnown(info); + mResult.set(new AppWorkaroundsUtils(info)); } } diff --git a/java/src/com/android/inputmethod/latin/utils/TextRange.java b/java/src/com/android/inputmethod/latin/utils/TextRange.java index 48b443ddd..dbf3b5060 100644 --- a/java/src/com/android/inputmethod/latin/utils/TextRange.java +++ b/java/src/com/android/inputmethod/latin/utils/TextRange.java @@ -31,6 +31,7 @@ public final class TextRange { private final int mCursorIndex; public final CharSequence mWord; + public final boolean mHasUrlSpans; public int getNumberOfCharsInWordBeforeCursor() { return mCursorIndex - mWordAtCursorStartIndex; @@ -95,7 +96,7 @@ public final class TextRange { } } if (spanStart == mWordAtCursorStartIndex && spanEnd == mWordAtCursorEndIndex) { - // If the span does not start and stop here, we ignore it. It probably extends + // If the span does not start and stop here, ignore it. It probably extends // past the start or end of the word, as happens in missing space correction // or EasyEditSpans put by voice input. spans[writeIndex++] = spans[readIndex]; @@ -105,7 +106,7 @@ public final class TextRange { } public TextRange(final CharSequence textAtCursor, final int wordAtCursorStartIndex, - final int wordAtCursorEndIndex, final int cursorIndex) { + final int wordAtCursorEndIndex, final int cursorIndex, final boolean hasUrlSpans) { if (wordAtCursorStartIndex < 0 || cursorIndex < wordAtCursorStartIndex || cursorIndex > wordAtCursorEndIndex || wordAtCursorEndIndex > textAtCursor.length()) { @@ -115,6 +116,7 @@ public final class TextRange { mWordAtCursorStartIndex = wordAtCursorStartIndex; mWordAtCursorEndIndex = wordAtCursorEndIndex; mCursorIndex = cursorIndex; + mHasUrlSpans = hasUrlSpans; mWord = mTextAtCursor.subSequence(mWordAtCursorStartIndex, mWordAtCursorEndIndex); } }
\ No newline at end of file diff --git a/java/src/com/android/inputmethod/latin/utils/TypefaceUtils.java b/java/src/com/android/inputmethod/latin/utils/TypefaceUtils.java index 47ea1ea75..fafba79c2 100644 --- a/java/src/com/android/inputmethod/latin/utils/TypefaceUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/TypefaceUtils.java @@ -22,16 +22,19 @@ import android.graphics.Typeface; import android.util.SparseArray; public final class TypefaceUtils { + private static final char[] KEY_LABEL_REFERENCE_CHAR = { 'M' }; + private static final char[] KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR = { '8' }; + private TypefaceUtils() { // This utility class is not publicly instantiable. } // This sparse array caches key label text height in pixel indexed by key label text size. - private static final SparseArray<Float> sTextHeightCache = CollectionUtils.newSparseArray(); + private static final SparseArray<Float> sTextHeightCache = new SparseArray<>(); // Working variable for the following method. private static final Rect sTextHeightBounds = new Rect(); - public static float getCharHeight(final char[] referenceChar, final Paint paint) { + private static float getCharHeight(final char[] referenceChar, final Paint paint) { final int key = getCharGeometryCacheKey(referenceChar[0], paint); synchronized (sTextHeightCache) { final Float cachedValue = sTextHeightCache.get(key); @@ -47,11 +50,11 @@ public final class TypefaceUtils { } // 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 SparseArray<Float> sTextWidthCache = new SparseArray<>(); // Working variable for the following method. private static final Rect sTextWidthBounds = new Rect(); - public static float getCharWidth(final char[] referenceChar, final Paint paint) { + private static float getCharWidth(final char[] referenceChar, final Paint paint) { final int key = getCharGeometryCacheKey(referenceChar[0], paint); synchronized (sTextWidthCache) { final Float cachedValue = sTextWidthCache.get(key); @@ -66,11 +69,6 @@ public final class TypefaceUtils { } } - public static float getStringWidth(final String string, final Paint paint) { - paint.getTextBounds(string, 0, string.length(), sTextWidthBounds); - return sTextWidthBounds.width(); - } - private static int getCharGeometryCacheKey(final char referenceChar, final Paint paint) { final int labelSize = (int)paint.getTextSize(); final Typeface face = paint.getTypeface(); @@ -86,9 +84,25 @@ public final class TypefaceUtils { } } - public static float getLabelWidth(final String label, final Paint paint) { - final Rect textBounds = new Rect(); - paint.getTextBounds(label, 0, label.length(), textBounds); - return textBounds.width(); + public static float getReferenceCharHeight(final Paint paint) { + return getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint); + } + + public static float getReferenceCharWidth(final Paint paint) { + return getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint); + } + + public static float getReferenceDigitWidth(final Paint paint) { + return getCharWidth(KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR, paint); + } + + // Working variable for the following method. + private static final Rect sStringWidthBounds = new Rect(); + + public static float getStringWidth(final String string, final Paint paint) { + synchronized (sStringWidthBounds) { + paint.getTextBounds(string, 0, string.length(), sStringWidthBounds); + return sStringWidthBounds.width(); + } } } diff --git a/java/src/com/android/inputmethod/latin/utils/UncachedInputMethodManagerUtils.java b/java/src/com/android/inputmethod/latin/utils/UncachedInputMethodManagerUtils.java new file mode 100644 index 000000000..5df00efb9 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/UncachedInputMethodManagerUtils.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.utils; + +import android.content.Context; +import android.provider.Settings; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodManager; + +/* + * A utility class for {@link InputMethodManager}. Unlike {@link RichInputMethodManager}, this + * class provides synchronous, non-cached access to {@link InputMethodManager}. The setup activity + * is a good example to use this class because {@link InputMethodManagerService} may not be aware of + * this IME immediately after this IME is installed. + */ +public final class UncachedInputMethodManagerUtils { + /** + * Check if the IME specified by the context is enabled. + * CAVEAT: This may cause a round trip IPC. + * + * @param context package context of the IME to be checked. + * @param imm the {@link InputMethodManager}. + * @return true if this IME is enabled. + */ + public static boolean isThisImeEnabled(final Context context, + final InputMethodManager imm) { + final String packageName = context.getPackageName(); + for (final InputMethodInfo imi : imm.getEnabledInputMethodList()) { + if (packageName.equals(imi.getPackageName())) { + return true; + } + } + return false; + } + + /** + * Check if the IME specified by the context is the current IME. + * CAVEAT: This may cause a round trip IPC. + * + * @param context package context of the IME to be checked. + * @param imm the {@link InputMethodManager}. + * @return true if this IME is the current IME. + */ + public static boolean isThisImeCurrent(final Context context, + final InputMethodManager imm) { + final InputMethodInfo imi = getInputMethodInfoOf(context.getPackageName(), imm); + final String currentImeId = Settings.Secure.getString( + context.getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD); + return imi != null && imi.getId().equals(currentImeId); + } + + /** + * Get {@link InputMethodInfo} of the IME specified by the package name. + * CAVEAT: This may cause a round trip IPC. + * + * @param packageName package name of the IME. + * @param imm the {@link InputMethodManager}. + * @return the {@link InputMethodInfo} of the IME specified by the <code>packageName</code>, + * or null if not found. + */ + public static InputMethodInfo getInputMethodInfoOf(final String packageName, + final InputMethodManager imm) { + for (final InputMethodInfo imi : imm.getInputMethodList()) { + if (packageName.equals(imi.getPackageName())) { + return imi; + } + } + return null; + } +} diff --git a/java/src/com/android/inputmethod/latin/utils/UsabilityStudyLogUtils.java b/java/src/com/android/inputmethod/latin/utils/UsabilityStudyLogUtils.java deleted file mode 100644 index 06826dac0..000000000 --- a/java/src/com/android/inputmethod/latin/utils/UsabilityStudyLogUtils.java +++ /dev/null @@ -1,293 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.utils; - -import android.content.Intent; -import android.content.pm.PackageManager; -import android.inputmethodservice.InputMethodService; -import android.net.Uri; -import android.os.Environment; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Process; -import android.util.Log; -import android.view.MotionEvent; - -import com.android.inputmethod.latin.LatinImeLogger; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.FileReader; -import java.io.IOException; -import java.io.PrintWriter; -import java.nio.channels.FileChannel; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - -public final class UsabilityStudyLogUtils { - // TODO: remove code duplication with ResearchLog class - private static final String USABILITY_TAG = UsabilityStudyLogUtils.class.getSimpleName(); - private static final String FILENAME = "log.txt"; - private final Handler mLoggingHandler; - private File mFile; - private File mDirectory; - private InputMethodService mIms; - private PrintWriter mWriter; - private final Date mDate; - private final SimpleDateFormat mDateFormat; - - private UsabilityStudyLogUtils() { - mDate = new Date(); - mDateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss.SSSZ", Locale.US); - - HandlerThread handlerThread = new HandlerThread("UsabilityStudyLogUtils logging task", - Process.THREAD_PRIORITY_BACKGROUND); - handlerThread.start(); - mLoggingHandler = new Handler(handlerThread.getLooper()); - } - - // Initialization-on-demand holder - private static final class OnDemandInitializationHolder { - public static final UsabilityStudyLogUtils sInstance = new UsabilityStudyLogUtils(); - } - - public static UsabilityStudyLogUtils getInstance() { - return OnDemandInitializationHolder.sInstance; - } - - public void init(final InputMethodService ims) { - mIms = ims; - mDirectory = ims.getFilesDir(); - } - - private void createLogFileIfNotExist() { - if ((mFile == null || !mFile.exists()) - && (mDirectory != null && mDirectory.exists())) { - try { - mWriter = getPrintWriter(mDirectory, FILENAME, false); - } catch (final IOException e) { - Log.e(USABILITY_TAG, "Can't create log file."); - } - } - } - - public static void writeBackSpace(final int x, final int y) { - UsabilityStudyLogUtils.getInstance().write("<backspace>\t" + x + "\t" + y); - } - - public static void writeChar(final char c, final int x, final int y) { - String inputChar = String.valueOf(c); - switch (c) { - case '\n': - inputChar = "<enter>"; - break; - case '\t': - inputChar = "<tab>"; - break; - case ' ': - inputChar = "<space>"; - break; - } - UsabilityStudyLogUtils.getInstance().write(inputChar + "\t" + x + "\t" + y); - LatinImeLogger.onPrintAllUsabilityStudyLogs(); - } - - public static void writeMotionEvent(final MotionEvent me) { - final int action = me.getActionMasked(); - final long eventTime = me.getEventTime(); - final int pointerCount = me.getPointerCount(); - for (int index = 0; index < pointerCount; index++) { - final int id = me.getPointerId(index); - final int x = (int)me.getX(index); - final int y = (int)me.getY(index); - final float size = me.getSize(index); - final float pressure = me.getPressure(index); - - final String eventTag; - switch (action) { - case MotionEvent.ACTION_UP: - eventTag = "[Up]"; - break; - case MotionEvent.ACTION_DOWN: - eventTag = "[Down]"; - break; - case MotionEvent.ACTION_POINTER_UP: - eventTag = "[PointerUp]"; - break; - case MotionEvent.ACTION_POINTER_DOWN: - eventTag = "[PointerDown]"; - break; - case MotionEvent.ACTION_MOVE: - eventTag = "[Move]"; - break; - default: - eventTag = "[Action" + action + "]"; - break; - } - getInstance().write(eventTag + eventTime + "," + id + "," + x + "," + y + "," + size - + "," + pressure); - } - } - - public void write(final String log) { - mLoggingHandler.post(new Runnable() { - @Override - public void run() { - createLogFileIfNotExist(); - final long currentTime = System.currentTimeMillis(); - mDate.setTime(currentTime); - - final String printString = String.format(Locale.US, "%s\t%d\t%s\n", - mDateFormat.format(mDate), currentTime, log); - if (LatinImeLogger.sDBG) { - Log.d(USABILITY_TAG, "Write: " + log); - } - mWriter.print(printString); - } - }); - } - - private synchronized String getBufferedLogs() { - mWriter.flush(); - final StringBuilder sb = new StringBuilder(); - final BufferedReader br = getBufferedReader(); - String line; - try { - while ((line = br.readLine()) != null) { - sb.append('\n'); - sb.append(line); - } - } catch (final IOException e) { - Log.e(USABILITY_TAG, "Can't read log file."); - } finally { - if (LatinImeLogger.sDBG) { - Log.d(USABILITY_TAG, "Got all buffered logs\n" + sb.toString()); - } - try { - br.close(); - } catch (final IOException e) { - // ignore. - } - } - return sb.toString(); - } - - public void emailResearcherLogsAll() { - mLoggingHandler.post(new Runnable() { - @Override - public void run() { - final Date date = new Date(); - date.setTime(System.currentTimeMillis()); - final String currentDateTimeString = - new SimpleDateFormat("yyyyMMdd-HHmmssZ", Locale.US).format(date); - if (mFile == null) { - Log.w(USABILITY_TAG, "No internal log file found."); - return; - } - if (mIms.checkCallingOrSelfPermission( - android.Manifest.permission.WRITE_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { - Log.w(USABILITY_TAG, "Doesn't have the permission WRITE_EXTERNAL_STORAGE"); - return; - } - mWriter.flush(); - final String destPath = Environment.getExternalStorageDirectory() - + "/research-" + currentDateTimeString + ".log"; - final File destFile = new File(destPath); - try { - final FileInputStream srcStream = new FileInputStream(mFile); - final FileOutputStream destStream = new FileOutputStream(destFile); - final FileChannel src = srcStream.getChannel(); - final FileChannel dest = destStream.getChannel(); - src.transferTo(0, src.size(), dest); - src.close(); - srcStream.close(); - dest.close(); - destStream.close(); - } catch (final FileNotFoundException e1) { - Log.w(USABILITY_TAG, e1); - return; - } catch (final IOException e2) { - Log.w(USABILITY_TAG, e2); - return; - } - if (!destFile.exists()) { - Log.w(USABILITY_TAG, "Dest file doesn't exist."); - return; - } - final Intent intent = new Intent(Intent.ACTION_SEND); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - if (LatinImeLogger.sDBG) { - Log.d(USABILITY_TAG, "Destination file URI is " + destFile.toURI()); - } - intent.setType("text/plain"); - intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + destPath)); - intent.putExtra(Intent.EXTRA_SUBJECT, - "[Research Logs] " + currentDateTimeString); - mIms.startActivity(intent); - } - }); - } - - public void printAll() { - mLoggingHandler.post(new Runnable() { - @Override - public void run() { - mIms.getCurrentInputConnection().commitText(getBufferedLogs(), 0); - } - }); - } - - public void clearAll() { - mLoggingHandler.post(new Runnable() { - @Override - public void run() { - if (mFile != null && mFile.exists()) { - if (LatinImeLogger.sDBG) { - Log.d(USABILITY_TAG, "Delete log file."); - } - mFile.delete(); - mWriter.close(); - } - } - }); - } - - private BufferedReader getBufferedReader() { - createLogFileIfNotExist(); - try { - return new BufferedReader(new FileReader(mFile)); - } catch (final FileNotFoundException e) { - return null; - } - } - - private PrintWriter getPrintWriter(final File dir, final String filename, - final boolean renew) throws IOException { - mFile = new File(dir, filename); - if (mFile.exists()) { - if (renew) { - mFile.delete(); - } - } - return new PrintWriter(new FileOutputStream(mFile), true /* autoFlush */); - } -} diff --git a/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java b/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java deleted file mode 100644 index 635afe7cc..000000000 --- a/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.utils; - -import android.util.Log; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.makedict.BinaryDictIOUtils; -import com.android.inputmethod.latin.makedict.DictDecoder; -import com.android.inputmethod.latin.makedict.DictEncoder; -import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray; -import com.android.inputmethod.latin.makedict.PendingAttribute; -import com.android.inputmethod.latin.makedict.UnsupportedFormatException; -import com.android.inputmethod.latin.personalization.UserHistoryDictionaryBigramList; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map.Entry; -import java.util.TreeMap; -import java.util.concurrent.TimeUnit; - -/** - * Reads and writes Binary files for a UserHistoryDictionary. - * - * All the methods in this class are static. - */ -public final class UserHistoryDictIOUtils { - private static final String TAG = UserHistoryDictIOUtils.class.getSimpleName(); - private static final boolean DEBUG = false; - private static final String USES_FORGETTING_CURVE_KEY = "USES_FORGETTING_CURVE"; - private static final String USES_FORGETTING_CURVE_VALUE = "1"; - private static final String LAST_UPDATED_TIME_KEY = "date"; - - public interface OnAddWordListener { - /** - * Callback to be notified when a word is added to the dictionary. - * @param word The added word. - * @param shortcutTarget A shortcut target for this word, or null if none. - * @param frequency The frequency for this word. - * @param shortcutFreq The frequency of the shortcut (0~15, with 15 = whitelist). - * Unspecified if shortcutTarget is null - do not rely on its value. - */ - public void setUnigram(final String word, final String shortcutTarget, final int frequency, - final int shortcutFreq); - public void setBigram(final String word1, final String word2, final int frequency); - } - - @UsedForTesting - public interface BigramDictionaryInterface { - public int getFrequency(final String word1, final String word2); - } - - /** - * Writes dictionary to file. - */ - public static void writeDictionary(final DictEncoder dictEncoder, - final BigramDictionaryInterface dict, final UserHistoryDictionaryBigramList bigrams, - final FormatOptions formatOptions) { - final FusionDictionary fusionDict = constructFusionDictionary(dict, bigrams); - fusionDict.addOptionAttribute(USES_FORGETTING_CURVE_KEY, USES_FORGETTING_CURVE_VALUE); - fusionDict.addOptionAttribute(LAST_UPDATED_TIME_KEY, - String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()))); - try { - dictEncoder.writeDictionary(fusionDict, formatOptions); - Log.d(TAG, "end writing"); - } catch (IOException e) { - Log.e(TAG, "IO exception while writing file", e); - } catch (UnsupportedFormatException e) { - Log.e(TAG, "Unsupported format", e); - } - } - - /** - * Constructs a new FusionDictionary from BigramDictionaryInterface. - */ - @UsedForTesting - static FusionDictionary constructFusionDictionary( - final BigramDictionaryInterface dict, final UserHistoryDictionaryBigramList bigrams) { - final FusionDictionary fusionDict = new FusionDictionary(new PtNodeArray(), - new FusionDictionary.DictionaryOptions(new HashMap<String, String>(), false, - false)); - int profTotal = 0; - for (final String word1 : bigrams.keySet()) { - final HashMap<String, Byte> word1Bigrams = bigrams.getBigrams(word1); - for (final String word2 : word1Bigrams.keySet()) { - final int freq = dict.getFrequency(word1, word2); - if (freq == -1) { - // don't add this bigram. - continue; - } - if (DEBUG) { - if (word1 == null) { - Log.d(TAG, "add unigram: " + word2 + "," + Integer.toString(freq)); - } else { - Log.d(TAG, "add bigram: " + word1 - + "," + word2 + "," + Integer.toString(freq)); - } - profTotal++; - } - if (word1 == null) { // unigram - fusionDict.add(word2, freq, null, false /* isNotAWord */); - } else { // bigram - if (FusionDictionary.findWordInTree(fusionDict.mRootNodeArray, word1) == null) { - fusionDict.add(word1, 2, null, false /* isNotAWord */); - } - fusionDict.setBigram(word1, word2, freq); - } - bigrams.updateBigram(word1, word2, (byte)freq); - } - } - if (DEBUG) { - Log.d(TAG, "add " + profTotal + "words"); - } - return fusionDict; - } - - /** - * Reads dictionary from file. - */ - public static void readDictionaryBinary(final DictDecoder dictDecoder, - final OnAddWordListener dict) { - final TreeMap<Integer, String> unigrams = CollectionUtils.newTreeMap(); - final TreeMap<Integer, Integer> frequencies = CollectionUtils.newTreeMap(); - final TreeMap<Integer, ArrayList<PendingAttribute>> bigrams = CollectionUtils.newTreeMap(); - try { - dictDecoder.readUnigramsAndBigramsBinary(unigrams, frequencies, bigrams); - } catch (IOException e) { - Log.e(TAG, "IO exception while reading file", e); - } catch (UnsupportedFormatException e) { - Log.e(TAG, "Unsupported format", e); - } catch (ArrayIndexOutOfBoundsException e) { - Log.e(TAG, "ArrayIndexOutOfBoundsException while reading file", e); - } - addWordsFromWordMap(unigrams, frequencies, bigrams, dict); - } - - /** - * Adds all unigrams and bigrams in maps to OnAddWordListener. - */ - @UsedForTesting - static void addWordsFromWordMap(final TreeMap<Integer, String> unigrams, - final TreeMap<Integer, Integer> frequencies, - final TreeMap<Integer, ArrayList<PendingAttribute>> bigrams, - final OnAddWordListener to) { - for (Entry<Integer, String> entry : unigrams.entrySet()) { - final String word1 = entry.getValue(); - final int unigramFrequency = frequencies.get(entry.getKey()); - to.setUnigram(word1, null /* shortcutTarget */, unigramFrequency, 0 /* shortcutFreq */); - final ArrayList<PendingAttribute> attrList = bigrams.get(entry.getKey()); - if (attrList != null) { - for (final PendingAttribute attr : attrList) { - final String word2 = unigrams.get(attr.mAddress); - if (word1 == null || word2 == null) { - Log.e(TAG, "Invalid bigram pair detected: " + word1 + ", " + word2); - continue; - } - to.setBigram(word1, word2, - BinaryDictIOUtils.reconstructBigramFrequency(unigramFrequency, - attr.mFrequency)); - } - } - } - - } -} diff --git a/java/src/com/android/inputmethod/latin/utils/UserHistoryForgettingCurveUtils.java b/java/src/com/android/inputmethod/latin/utils/UserHistoryForgettingCurveUtils.java deleted file mode 100644 index 1992b2f5d..000000000 --- a/java/src/com/android/inputmethod/latin/utils/UserHistoryForgettingCurveUtils.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.utils; - -import android.util.Log; - -import java.util.concurrent.TimeUnit; - -public final class UserHistoryForgettingCurveUtils { - private static final String TAG = UserHistoryForgettingCurveUtils.class.getSimpleName(); - private static final boolean DEBUG = false; - private static final int DEFAULT_FC_FREQ = 127; - private static final int BOOSTED_FC_FREQ = 200; - private static int FC_FREQ_MAX = DEFAULT_FC_FREQ; - /* package */ static final int COUNT_MAX = 3; - private static final int FC_LEVEL_MAX = 3; - /* package */ static final int ELAPSED_TIME_MAX = 15; - private static final int ELAPSED_TIME_INTERVAL_HOURS = 6; - private static final long ELAPSED_TIME_INTERVAL_MILLIS = - TimeUnit.HOURS.toMillis(ELAPSED_TIME_INTERVAL_HOURS); - private static final int HALF_LIFE_HOURS = 48; - private static final int MAX_PUSH_ELAPSED = (FC_LEVEL_MAX + 1) * (ELAPSED_TIME_MAX + 1); - - public static void boostMaxFreqForDebug() { - FC_FREQ_MAX = BOOSTED_FC_FREQ; - } - - public static void resetMaxFreqForDebug() { - FC_FREQ_MAX = DEFAULT_FC_FREQ; - } - - private UserHistoryForgettingCurveUtils() { - // This utility class is not publicly instantiable. - } - - public static final class ForgettingCurveParams { - private byte mFc; - long mLastTouchedTime = 0; - private final boolean mIsValid; - - private void updateLastTouchedTime() { - mLastTouchedTime = System.currentTimeMillis(); - } - - public ForgettingCurveParams(boolean isValid) { - this(System.currentTimeMillis(), isValid); - } - - private ForgettingCurveParams(long now, boolean isValid) { - this(pushCount((byte)0, isValid), now, now, isValid); - } - - /** This constructor is called when the user history bigram dictionary is being restored. */ - public ForgettingCurveParams(int fc, long now, long last) { - // All words with level >= 1 had been saved. - // Invalid words with level == 0 had been saved. - // Valid words words with level == 0 had *not* been saved. - this(fc, now, last, fcToLevel((byte)fc) > 0); - } - - private ForgettingCurveParams(int fc, long now, long last, boolean isValid) { - mIsValid = isValid; - mFc = (byte)fc; - mLastTouchedTime = last; - updateElapsedTime(now); - } - - public boolean isValid() { - return mIsValid; - } - - public byte getFc() { - updateElapsedTime(System.currentTimeMillis()); - return mFc; - } - - public int getFrequency() { - updateElapsedTime(System.currentTimeMillis()); - return UserHistoryForgettingCurveUtils.fcToFreq(mFc); - } - - public int notifyTypedAgainAndGetFrequency() { - updateLastTouchedTime(); - // TODO: Check whether this word is valid or not - mFc = pushCount(mFc, false); - return UserHistoryForgettingCurveUtils.fcToFreq(mFc); - } - - private void updateElapsedTime(long now) { - final int elapsedTimeCount = - (int)((now - mLastTouchedTime) / ELAPSED_TIME_INTERVAL_MILLIS); - if (elapsedTimeCount <= 0) { - return; - } - if (elapsedTimeCount >= MAX_PUSH_ELAPSED) { - mLastTouchedTime = now; - mFc = 0; - return; - } - for (int i = 0; i < elapsedTimeCount; ++i) { - mLastTouchedTime += ELAPSED_TIME_INTERVAL_MILLIS; - mFc = pushElapsedTime(mFc); - } - } - } - - /* package */ static int fcToElapsedTime(byte fc) { - return fc & 0x0F; - } - - /* package */ static int fcToCount(byte fc) { - return (fc >> 4) & 0x03; - } - - /* package */ static int fcToLevel(byte fc) { - return (fc >> 6) & 0x03; - } - - private static int calcFreq(int elapsedTime, int count, int level) { - if (level <= 0) { - // Reserved words, just return -1 - return -1; - } - if (count == COUNT_MAX) { - // Temporary promote because it's frequently typed recently - ++level; - } - final int et = Math.min(FC_FREQ_MAX, Math.max(0, elapsedTime)); - final int l = Math.min(FC_LEVEL_MAX, Math.max(0, level)); - return MathUtils.SCORE_TABLE[l - 1][et]; - } - - /* pakcage */ static byte calcFc(int elapsedTime, int count, int level) { - final int et = Math.min(FC_FREQ_MAX, Math.max(0, elapsedTime)); - final int c = Math.min(COUNT_MAX, Math.max(0, count)); - final int l = Math.min(FC_LEVEL_MAX, Math.max(0, level)); - return (byte)(et | (c << 4) | (l << 6)); - } - - public static int fcToFreq(byte fc) { - final int elapsedTime = fcToElapsedTime(fc); - final int count = fcToCount(fc); - final int level = fcToLevel(fc); - return calcFreq(elapsedTime, count, level); - } - - public static byte pushElapsedTime(byte fc) { - int elapsedTime = fcToElapsedTime(fc); - int count = fcToCount(fc); - int level = fcToLevel(fc); - if (elapsedTime >= ELAPSED_TIME_MAX) { - // Downgrade level - elapsedTime = 0; - count = COUNT_MAX; - --level; - } else { - ++elapsedTime; - } - return calcFc(elapsedTime, count, level); - } - - public static byte pushCount(byte fc, boolean isValid) { - final int elapsedTime = fcToElapsedTime(fc); - int count = fcToCount(fc); - int level = fcToLevel(fc); - if ((elapsedTime == 0 && count >= COUNT_MAX) || (isValid && level == 0)) { - // Upgrade level - ++level; - count = 0; - if (DEBUG) { - Log.d(TAG, "Upgrade level."); - } - } else { - ++count; - } - return calcFc(0, count, level); - } - - // TODO: isValid should be false for a word whose frequency is 0, - // or that is not in the dictionary. - /** - * Check wheather we should save the bigram to the SQL DB or not - */ - public static boolean needsToSave(byte fc, boolean isValid, boolean addLevel0Bigram) { - int level = fcToLevel(fc); - if (level == 0) { - if (isValid || !addLevel0Bigram) { - return false; - } - } - final int elapsedTime = fcToElapsedTime(fc); - return (elapsedTime < ELAPSED_TIME_MAX - 1 || level > 0); - } - - private static final class MathUtils { - public static final int[][] SCORE_TABLE = new int[FC_LEVEL_MAX][ELAPSED_TIME_MAX + 1]; - static { - for (int i = 0; i < FC_LEVEL_MAX; ++i) { - final float initialFreq; - if (i >= 2) { - initialFreq = FC_FREQ_MAX; - } else if (i == 1) { - initialFreq = FC_FREQ_MAX / 2; - } else if (i == 0) { - initialFreq = FC_FREQ_MAX / 4; - } else { - continue; - } - for (int j = 0; j < ELAPSED_TIME_MAX; ++j) { - final float elapsedHours = j * ELAPSED_TIME_INTERVAL_HOURS; - final float freq = initialFreq - * (float)Math.pow(initialFreq, elapsedHours / HALF_LIFE_HOURS); - final int intFreq = Math.min(FC_FREQ_MAX, Math.max(0, (int)freq)); - SCORE_TABLE[i][j] = intFreq; - } - } - } - } -} diff --git a/java/src/com/android/inputmethod/latin/utils/UserLogRingCharBuffer.java b/java/src/com/android/inputmethod/latin/utils/UserLogRingCharBuffer.java deleted file mode 100644 index a75d353c9..000000000 --- a/java/src/com/android/inputmethod/latin/utils/UserLogRingCharBuffer.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.utils; - -import android.inputmethodservice.InputMethodService; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.LatinImeLogger; -import com.android.inputmethod.latin.settings.Settings; - -public final class UserLogRingCharBuffer { - public /* for test */ static final int BUFSIZE = 20; - public /* for test */ int mLength = 0; - - private static UserLogRingCharBuffer sUserLogRingCharBuffer = new UserLogRingCharBuffer(); - private static final char PLACEHOLDER_DELIMITER_CHAR = '\uFFFC'; - private static final int INVALID_COORDINATE = -2; - private boolean mEnabled = false; - private int mEnd = 0; - private char[] mCharBuf = new char[BUFSIZE]; - private int[] mXBuf = new int[BUFSIZE]; - private int[] mYBuf = new int[BUFSIZE]; - - private UserLogRingCharBuffer() { - // Intentional empty constructor for singleton. - } - - @UsedForTesting - public static UserLogRingCharBuffer getInstance() { - return sUserLogRingCharBuffer; - } - - public static UserLogRingCharBuffer init(final InputMethodService context, - final boolean enabled, final boolean usabilityStudy) { - if (!(enabled || usabilityStudy)) { - return null; - } - sUserLogRingCharBuffer.mEnabled = true; - UsabilityStudyLogUtils.getInstance().init(context); - return sUserLogRingCharBuffer; - } - - private static int normalize(final int in) { - int ret = in % BUFSIZE; - return ret < 0 ? ret + BUFSIZE : ret; - } - - // TODO: accept code points - @UsedForTesting - public void push(final char c, final int x, final int y) { - if (!mEnabled) { - return; - } - if (LatinImeLogger.sUsabilityStudy) { - UsabilityStudyLogUtils.getInstance().writeChar(c, x, y); - } - mCharBuf[mEnd] = c; - mXBuf[mEnd] = x; - mYBuf[mEnd] = y; - mEnd = normalize(mEnd + 1); - if (mLength < BUFSIZE) { - ++mLength; - } - } - - public char pop() { - if (mLength < 1) { - return PLACEHOLDER_DELIMITER_CHAR; - } - mEnd = normalize(mEnd - 1); - --mLength; - return mCharBuf[mEnd]; - } - - public char getBackwardNthChar(final int n) { - if (mLength <= n || n < 0) { - return PLACEHOLDER_DELIMITER_CHAR; - } - return mCharBuf[normalize(mEnd - n - 1)]; - } - - public int getPreviousX(final char c, final int back) { - final int index = normalize(mEnd - 2 - back); - if (mLength <= back - || Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) { - return INVALID_COORDINATE; - } - return mXBuf[index]; - } - - public int getPreviousY(final char c, final int back) { - int index = normalize(mEnd - 2 - back); - if (mLength <= back - || Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) { - return INVALID_COORDINATE; - } - return mYBuf[index]; - } - - public String getLastWord(final int ignoreCharCount) { - final StringBuilder sb = new StringBuilder(); - int i = ignoreCharCount; - for (; i < mLength; ++i) { - final char c = mCharBuf[normalize(mEnd - 1 - i)]; - if (!Settings.getInstance().isWordSeparator(c)) { - break; - } - } - for (; i < mLength; ++i) { - char c = mCharBuf[normalize(mEnd - 1 - i)]; - if (!Settings.getInstance().isWordSeparator(c)) { - sb.append(c); - } else { - break; - } - } - return sb.reverse().toString(); - } - - public void reset() { - mLength = 0; - } -} diff --git a/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java b/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java deleted file mode 100644 index 4f86526a7..000000000 --- a/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java +++ /dev/null @@ -1,34 +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.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(final Context context, final Intent intent) { - if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) { - UploaderService.cancelAndRescheduleUploadingService(context, - true /* needsRescheduling */); - } - } -} diff --git a/java/src/com/android/inputmethod/research/FeedbackActivity.java b/java/src/com/android/inputmethod/research/FeedbackActivity.java deleted file mode 100644 index 520b88d2f..000000000 --- a/java/src/com/android/inputmethod/research/FeedbackActivity.java +++ /dev/null @@ -1,38 +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.research; - -import android.app.Activity; -import android.os.Bundle; - -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); - layout.setActivity(this); - } - - @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 deleted file mode 100644 index 75fbbf1ba..000000000 --- a/java/src/com/android/inputmethod/research/FeedbackFragment.java +++ /dev/null @@ -1,131 +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.research; - -import android.app.Fragment; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextUtils; -import android.util.Log; -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 android.widget.Toast; - -import com.android.inputmethod.latin.R; - -public class FeedbackFragment extends Fragment implements OnClickListener { - private static final String TAG = FeedbackFragment.class.getSimpleName(); - - public static final String KEY_FEEDBACK_STRING = "FeedbackString"; - public static final String KEY_INCLUDE_ACCOUNT_NAME = "IncludeAccountName"; - public static final String KEY_HAS_USER_RECORDING = "HasRecording"; - - private EditText mEditText; - private CheckBox mIncludingAccountNameCheckBox; - private CheckBox mIncludingUserRecordingCheckBox; - private Button mSendButton; - private Button mCancelButton; - - @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); - mEditText.requestFocus(); - mIncludingAccountNameCheckBox = (CheckBox) view.findViewById( - R.id.research_feedback_include_account_name); - mIncludingUserRecordingCheckBox = (CheckBox) view.findViewById( - R.id.research_feedback_include_recording_checkbox); - mIncludingUserRecordingCheckBox.setOnClickListener(this); - - mSendButton = (Button) view.findViewById(R.id.research_feedback_send_button); - mSendButton.setOnClickListener(this); - mCancelButton = (Button) view.findViewById(R.id.research_feedback_cancel_button); - mCancelButton.setOnClickListener(this); - - if (savedInstanceState != null) { - restoreState(savedInstanceState); - } else { - final Bundle bundle = getActivity().getIntent().getExtras(); - if (bundle != null) { - restoreState(bundle); - } - } - return view; - } - - @Override - public void onClick(final View view) { - final ResearchLogger researchLogger = ResearchLogger.getInstance(); - if (view == mIncludingUserRecordingCheckBox) { - if (mIncludingUserRecordingCheckBox.isChecked()) { - final Bundle bundle = new Bundle(); - onSaveInstanceState(bundle); - - // Let the user make a recording - getActivity().finish(); - - researchLogger.setFeedbackDialogBundle(bundle); - researchLogger.onLeavingSendFeedbackDialog(); - researchLogger.startRecording(); - } - } else if (view == mSendButton) { - final Editable editable = mEditText.getText(); - final String feedbackContents = editable.toString(); - if (TextUtils.isEmpty(feedbackContents)) { - Toast.makeText(getActivity(), - R.string.research_feedback_empty_feedback_error_message, - Toast.LENGTH_LONG).show(); - } else { - final boolean isIncludingAccountName = mIncludingAccountNameCheckBox.isChecked(); - researchLogger.sendFeedback(feedbackContents, false /* isIncludingHistory */, - isIncludingAccountName, mIncludingUserRecordingCheckBox.isChecked()); - getActivity().finish(); - researchLogger.setFeedbackDialogBundle(null); - researchLogger.onLeavingSendFeedbackDialog(); - } - } else if (view == mCancelButton) { - Log.d(TAG, "Finishing"); - getActivity().finish(); - researchLogger.setFeedbackDialogBundle(null); - researchLogger.onLeavingSendFeedbackDialog(); - } else { - Log.e(TAG, "Unknown view passed to FeedbackFragment.onClick()"); - } - } - - @Override - public void onSaveInstanceState(final Bundle bundle) { - final String savedFeedbackString = mEditText.getText().toString(); - - bundle.putString(KEY_FEEDBACK_STRING, savedFeedbackString); - bundle.putBoolean(KEY_INCLUDE_ACCOUNT_NAME, mIncludingAccountNameCheckBox.isChecked()); - bundle.putBoolean(KEY_HAS_USER_RECORDING, mIncludingUserRecordingCheckBox.isChecked()); - } - - private void restoreState(final Bundle bundle) { - mEditText.setText(bundle.getString(KEY_FEEDBACK_STRING)); - mIncludingAccountNameCheckBox.setChecked(bundle.getBoolean(KEY_INCLUDE_ACCOUNT_NAME)); - mIncludingUserRecordingCheckBox.setChecked(bundle.getBoolean(KEY_HAS_USER_RECORDING)); - } -} diff --git a/java/src/com/android/inputmethod/research/FeedbackLayout.java b/java/src/com/android/inputmethod/research/FeedbackLayout.java deleted file mode 100644 index d283d14b2..000000000 --- a/java/src/com/android/inputmethod/research/FeedbackLayout.java +++ /dev/null @@ -1,62 +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.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/FixedLogBuffer.java b/java/src/com/android/inputmethod/research/FixedLogBuffer.java deleted file mode 100644 index 8b64de8ae..000000000 --- a/java/src/com/android/inputmethod/research/FixedLogBuffer.java +++ /dev/null @@ -1,174 +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.research; - -import java.util.ArrayList; -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. - * - * This variant of a LogBuffer has a limited memory footprint because of its limited size. This - * makes it useful, for example, for recording a window of the user's most recent actions in case - * they want to report an observed error that they do not know how to reproduce. - */ -public class FixedLogBuffer extends LogBuffer { - /* package for test */ int mWordCapacity; - // The number of members of mLogUnits that are actual words. - private 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 - */ - public FixedLogBuffer(final int wordCapacity) { - super(); - if (wordCapacity <= 0) { - throw new IllegalArgumentException("wordCapacity must be 1 or greater."); - } - 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. - */ - @Override - public void shiftIn(final LogUnit newLogUnit) { - if (!newLogUnit.hasOneOrMoreWords()) { - // This LogUnit doesn't contain any word, so it doesn't count toward the word-limit. - super.shiftIn(newLogUnit); - return; - } - final int numWordsIncoming = newLogUnit.getNumWords(); - if (mNumActualWords >= mWordCapacity) { - // Give subclass a chance to handle the buffer full condition by shifting out logUnits. - // TODO: Tell onBufferFull() how much space it needs to make to avoid forced eviction. - onBufferFull(); - // If still full, evict. - if (mNumActualWords >= mWordCapacity) { - shiftOutWords(numWordsIncoming); - } - } - super.shiftIn(newLogUnit); - mNumActualWords += numWordsIncoming; - } - - @Override - public LogUnit unshiftIn() { - final LogUnit logUnit = super.unshiftIn(); - if (logUnit != null && logUnit.hasOneOrMoreWords()) { - mNumActualWords -= logUnit.getNumWords(); - } - return logUnit; - } - - public int getNumWords() { - return mNumActualWords; - } - - /** - * Removes all LogUnits from the buffer without calling onShiftOut(). - */ - @Override - public void clear() { - super.clear(); - mNumActualWords = 0; - } - - /** - * Called when the buffer has just shifted in one more word than its maximum, and its about to - * shift out LogUnits to bring it back down to the maximum. - * - * Base class does nothing; subclasses may override if they want to record non-privacy sensitive - * events that fall off the end. - */ - protected void onBufferFull() { - } - - @Override - public LogUnit shiftOut() { - final LogUnit logUnit = super.shiftOut(); - if (logUnit != null && logUnit.hasOneOrMoreWords()) { - mNumActualWords -= logUnit.getNumWords(); - } - return logUnit; - } - - /** - * Remove LogUnits from the front of the LogBuffer until {@code numWords} have been removed. - * - * If there are less than {@code numWords} in the buffer, shifts out all {@code LogUnit}s. - * - * @param numWords the minimum number of words in {@link LogUnit}s to shift out - * @return the number of actual words LogUnit}s shifted out - */ - protected int shiftOutWords(final int numWords) { - int numWordsShiftedOut = 0; - do { - final LogUnit logUnit = shiftOut(); - if (logUnit == null) break; - numWordsShiftedOut += logUnit.getNumWords(); - } while (numWordsShiftedOut < numWords); - return numWordsShiftedOut; - } - - public void shiftOutAll() { - final LinkedList<LogUnit> logUnits = getLogUnits(); - while (!logUnits.isEmpty()) { - shiftOut(); - } - mNumActualWords = 0; - } - - /** - * Returns a list of {@link LogUnit}s at the front of the buffer that have words associated with - * them. - * - * There will be no more than {@code n} words in the returned list. So if 2 words are - * requested, and the first LogUnit has 3 words, it is not returned. If 2 words are requested, - * and the first LogUnit has only 1 word, and the next LogUnit 2 words, only the first LogUnit - * is returned. If the first LogUnit has no words associated with it, and the second LogUnit - * has three words, then only the first LogUnit (which has no associated words) is returned. If - * there are not enough LogUnits in the buffer to meet the word requirement, then all LogUnits - * will be returned. - * - * @param n The maximum number of {@link LogUnit}s with words to return. - * @return The list of the {@link LogUnit}s containing the first n words - */ - public ArrayList<LogUnit> peekAtFirstNWords(int n) { - final LinkedList<LogUnit> logUnits = getLogUnits(); - // Allocate space for n*2 logUnits. There will be at least n, one for each word, and - // there may be additional for punctuation, between-word commands, etc. This should be - // enough that reallocation won't be necessary. - final ArrayList<LogUnit> resultList = new ArrayList<LogUnit>(n * 2); - for (final LogUnit logUnit : logUnits) { - n -= logUnit.getNumWords(); - if (n < 0) break; - resultList.add(logUnit); - } - return resultList; - } -} diff --git a/java/src/com/android/inputmethod/research/JsonUtils.java b/java/src/com/android/inputmethod/research/JsonUtils.java deleted file mode 100644 index 2beebdfae..000000000 --- a/java/src/com/android/inputmethod/research/JsonUtils.java +++ /dev/null @@ -1,162 +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.research; - -import android.content.SharedPreferences; -import android.util.JsonWriter; -import android.view.MotionEvent; -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 java.io.IOException; -import java.util.Map; - -/** - * Routines for mapping classes and variables to JSON representations for logging. - */ -/* package */ class JsonUtils { - private JsonUtils() { - // This utility class is not publicly instantiable. - } - - /* package */ static void writeJson(final CompletionInfo[] ci, final JsonWriter jsonWriter) - throws IOException { - jsonWriter.beginArray(); - for (int j = 0; j < ci.length; j++) { - jsonWriter.value(ci[j].toString()); - } - jsonWriter.endArray(); - } - - /* package */ static void writeJson(final SharedPreferences prefs, final JsonWriter jsonWriter) - throws IOException { - jsonWriter.beginObject(); - for (Map.Entry<String,?> entry : prefs.getAll().entrySet()) { - jsonWriter.name(entry.getKey()); - final Object innerValue = entry.getValue(); - if (innerValue == null) { - jsonWriter.nullValue(); - } else if (innerValue instanceof Boolean) { - jsonWriter.value((Boolean) innerValue); - } else if (innerValue instanceof Number) { - jsonWriter.value((Number) innerValue); - } else { - jsonWriter.value(innerValue.toString()); - } - } - jsonWriter.endObject(); - } - - /* package */ static void writeJson(final Key[] keys, final JsonWriter jsonWriter) - throws IOException { - jsonWriter.beginArray(); - for (Key key : keys) { - writeJson(key, jsonWriter); - } - jsonWriter.endArray(); - } - - private static void writeJson(final Key key, final JsonWriter jsonWriter) throws IOException { - jsonWriter.beginObject(); - jsonWriter.name("code").value(key.getCode()); - jsonWriter.name("altCode").value(key.getAltCode()); - jsonWriter.name("x").value(key.getX()); - jsonWriter.name("y").value(key.getY()); - jsonWriter.name("w").value(key.getWidth()); - jsonWriter.name("h").value(key.getHeight()); - jsonWriter.endObject(); - } - - /* package */ static void writeJson(final SuggestedWords words, final JsonWriter jsonWriter) - throws IOException { - jsonWriter.beginObject(); - jsonWriter.name("typedWordValid").value(words.mTypedWordValid); - jsonWriter.name("willAutoCorrect") - .value(words.mWillAutoCorrect); - jsonWriter.name("isPunctuationSuggestions") - .value(words.mIsPunctuationSuggestions); - jsonWriter.name("isObsoleteSuggestions").value(words.mIsObsoleteSuggestions); - jsonWriter.name("isPrediction").value(words.mIsPrediction); - jsonWriter.name("suggestedWords"); - jsonWriter.beginArray(); - final int size = words.size(); - for (int j = 0; j < size; j++) { - final SuggestedWordInfo wordInfo = words.getInfo(j); - jsonWriter.beginObject(); - jsonWriter.name("word").value(wordInfo.toString()); - jsonWriter.name("score").value(wordInfo.mScore); - jsonWriter.name("kind").value(wordInfo.mKind); - jsonWriter.name("sourceDict").value(wordInfo.mSourceDict.mDictType); - jsonWriter.endObject(); - } - jsonWriter.endArray(); - jsonWriter.endObject(); - } - - /* package */ static void writeJson(final MotionEvent me, final JsonWriter jsonWriter) - throws IOException { - jsonWriter.beginObject(); - jsonWriter.name("pointerIds"); - jsonWriter.beginArray(); - final int pointerCount = me.getPointerCount(); - for (int index = 0; index < pointerCount; index++) { - jsonWriter.value(me.getPointerId(index)); - } - jsonWriter.endArray(); - - jsonWriter.name("xyt"); - jsonWriter.beginArray(); - final int historicalSize = me.getHistorySize(); - for (int index = 0; index < historicalSize; index++) { - jsonWriter.beginObject(); - jsonWriter.name("t"); - jsonWriter.value(me.getHistoricalEventTime(index)); - jsonWriter.name("d"); - jsonWriter.beginArray(); - for (int pointerIndex = 0; pointerIndex < pointerCount; pointerIndex++) { - jsonWriter.beginObject(); - jsonWriter.name("x"); - jsonWriter.value(me.getHistoricalX(pointerIndex, index)); - jsonWriter.name("y"); - jsonWriter.value(me.getHistoricalY(pointerIndex, index)); - jsonWriter.endObject(); - } - jsonWriter.endArray(); - jsonWriter.endObject(); - } - jsonWriter.beginObject(); - jsonWriter.name("t"); - jsonWriter.value(me.getEventTime()); - jsonWriter.name("d"); - jsonWriter.beginArray(); - for (int pointerIndex = 0; pointerIndex < pointerCount; pointerIndex++) { - jsonWriter.beginObject(); - jsonWriter.name("x"); - jsonWriter.value(me.getX(pointerIndex)); - jsonWriter.name("y"); - jsonWriter.value(me.getY(pointerIndex)); - jsonWriter.endObject(); - } - jsonWriter.endArray(); - jsonWriter.endObject(); - jsonWriter.endArray(); - jsonWriter.endObject(); - } -} diff --git a/java/src/com/android/inputmethod/research/LogBuffer.java b/java/src/com/android/inputmethod/research/LogBuffer.java deleted file mode 100644 index b07b761f0..000000000 --- a/java/src/com/android/inputmethod/research/LogBuffer.java +++ /dev/null @@ -1,73 +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.research; - -import java.util.LinkedList; - -/** - * Maintain a FIFO queue of LogUnits. - * - * This class provides an unbounded queue. This is useful when the user is aware that their actions - * are being recorded, such as when they are trying to reproduce a bug. In this case, there should - * not be artificial restrictions on how many events that can be saved. - */ -public class LogBuffer { - // TODO: Gracefully handle situations in which this LogBuffer is consuming too much memory. - // This may happen, for example, if the user has forgotten that data is being logged. - private final LinkedList<LogUnit> mLogUnits; - - public LogBuffer() { - mLogUnits = new LinkedList<LogUnit>(); - } - - protected LinkedList<LogUnit> getLogUnits() { - return mLogUnits; - } - - public void clear() { - mLogUnits.clear(); - } - - public void shiftIn(final LogUnit logUnit) { - mLogUnits.add(logUnit); - } - - public LogUnit unshiftIn() { - if (mLogUnits.isEmpty()) { - return null; - } - return mLogUnits.removeLast(); - } - - public LogUnit peekLastLogUnit() { - if (mLogUnits.isEmpty()) { - return null; - } - return mLogUnits.peekLast(); - } - - public boolean isEmpty() { - return mLogUnits.isEmpty(); - } - - public LogUnit shiftOut() { - if (isEmpty()) { - return null; - } - return mLogUnits.removeFirst(); - } -} diff --git a/java/src/com/android/inputmethod/research/LogStatement.java b/java/src/com/android/inputmethod/research/LogStatement.java deleted file mode 100644 index 06b918af5..000000000 --- a/java/src/com/android/inputmethod/research/LogStatement.java +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.research; - -import android.content.SharedPreferences; -import android.util.JsonWriter; -import android.util.Log; -import android.view.MotionEvent; -import android.view.inputmethod.CompletionInfo; - -import com.android.inputmethod.keyboard.Key; -import com.android.inputmethod.latin.SuggestedWords; -import com.android.inputmethod.latin.define.ProductionFlag; - -import java.io.IOException; - -/** - * A template for typed information stored in the logs. - * - * A LogStatement contains a name, keys, and flags about whether the {@code Object[] values} - * associated with the {@code String[] keys} are likely to reveal information about the user. The - * actual values are stored separately. - */ -public class LogStatement { - private static final String TAG = LogStatement.class.getSimpleName(); - private static final boolean DEBUG = false - && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; - - // Constants for particular statements - public static final String TYPE_POINTER_TRACKER_CALL_LISTENER_ON_CODE_INPUT = - "PointerTrackerCallListenerOnCodeInput"; - public static final String KEY_CODE = "code"; - public static final String VALUE_RESEARCH = "research"; - public static final String TYPE_MAIN_KEYBOARD_VIEW_ON_LONG_PRESS = - "MainKeyboardViewOnLongPress"; - public static final String ACTION = "action"; - public static final String VALUE_DOWN = "DOWN"; - public static final String TYPE_MOTION_EVENT = "MotionEvent"; - public static final String KEY_IS_LOGGING_RELATED = "isLoggingRelated"; - - // Keys for internal key/value pairs - private static final String CURRENT_TIME_KEY = "_ct"; - private static final String UPTIME_KEY = "_ut"; - private static final String EVENT_TYPE_KEY = "_ty"; - - // Name specifying the LogStatement type. - private final String mType; - - // mIsPotentiallyPrivate indicates that 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. - private final boolean mIsPotentiallyPrivate; - - // mIsPotentiallyRevealing indicates that this statement may disclose details about other - // words typed in other LogUnits. This can happen if the user is not inserting spaces, and - // data from Suggestions and/or Composing text reveals the entire "megaword". For example, - // say the user is typing "for the win", and the system wants to record the bigram "the - // win". If the user types "forthe", omitting the space, the system will give "for the" as - // a suggestion. If the user accepts the autocorrection, the suggestion for "for the" is - // included in the log for the word "the", disclosing that the previous word had been "for". - // For now, we simply do not include this data when logging part of a "megaword". - private final boolean mIsPotentiallyRevealing; - - // mKeys stores the names that are the attributes in the output json objects - private final String[] mKeys; - private static final String[] NULL_KEYS = new String[0]; - - LogStatement(final String name, final boolean isPotentiallyPrivate, - final boolean isPotentiallyRevealing, final String... keys) { - mType = name; - mIsPotentiallyPrivate = isPotentiallyPrivate; - mIsPotentiallyRevealing = isPotentiallyRevealing; - mKeys = (keys == null) ? NULL_KEYS : keys; - } - - public String getType() { - return mType; - } - - public boolean isPotentiallyPrivate() { - return mIsPotentiallyPrivate; - } - - public boolean isPotentiallyRevealing() { - return mIsPotentiallyRevealing; - } - - public String[] getKeys() { - return mKeys; - } - - /** - * Utility function to test whether a key-value pair exists in a LogStatement. - * - * A LogStatement is really just a template -- it does not contain the values, only the - * keys. So the values must be passed in as an argument. - * - * @param queryKey the String that is tested by {@code String.equals()} to the keys in the - * LogStatement - * @param queryValue an Object that must be {@code Object.equals()} to the key's corresponding - * value in the {@code values} array - * @param values the values corresponding to mKeys - * - * @returns {@true} if {@code queryKey} exists in the keys for this LogStatement, and {@code - * queryValue} matches the corresponding value in {@code values} - * - * @throws IllegalArgumentException if {@code values.length} is not equal to keys().length() - */ - public boolean containsKeyValuePair(final String queryKey, final Object queryValue, - final Object[] values) { - if (mKeys.length != values.length) { - throw new IllegalArgumentException("Mismatched number of keys and values."); - } - final int length = mKeys.length; - for (int i = 0; i < length; i++) { - if (mKeys[i].equals(queryKey) && values[i].equals(queryValue)) { - return true; - } - } - return false; - } - - /** - * Utility function to set a value in a LogStatement. - * - * A LogStatement is really just a template -- it does not contain the values, only the - * keys. So the values must be passed in as an argument. - * - * @param queryKey the String that is tested by {@code String.equals()} to the keys in the - * LogStatement - * @param values the array of values corresponding to mKeys - * @param newValue the replacement value to go into the {@code values} array - * - * @returns {@true} if the key exists and the value was successfully set, {@false} otherwise - * - * @throws IllegalArgumentException if {@code values.length} is not equal to keys().length() - */ - public boolean setValue(final String queryKey, final Object[] values, final Object newValue) { - if (mKeys.length != values.length) { - throw new IllegalArgumentException("Mismatched number of keys and values."); - } - final int length = mKeys.length; - for (int i = 0; i < length; i++) { - if (mKeys[i].equals(queryKey)) { - values[i] = newValue; - return true; - } - } - return false; - } - - /** - * Write the contents out through jsonWriter. - * - * The JsonWriter class must have already had {@code JsonWriter.beginArray} called on it. - * - * Note that this method is not thread safe for the same jsonWriter. Callers must ensure - * thread safety. - */ - public boolean outputToLocked(final JsonWriter jsonWriter, final Long time, - final Object... values) { - if (DEBUG) { - if (mKeys.length != values.length) { - Log.d(TAG, "Key and Value list sizes do not match. " + mType); - } - } - try { - jsonWriter.beginObject(); - jsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis()); - jsonWriter.name(UPTIME_KEY).value(time); - jsonWriter.name(EVENT_TYPE_KEY).value(mType); - final int length = values.length; - for (int i = 0; i < length; i++) { - jsonWriter.name(mKeys[i]); - final Object value = values[i]; - if (value instanceof CharSequence) { - jsonWriter.value(value.toString()); - } else if (value instanceof Number) { - jsonWriter.value((Number) value); - } else if (value instanceof Boolean) { - jsonWriter.value((Boolean) value); - } else if (value instanceof CompletionInfo[]) { - JsonUtils.writeJson((CompletionInfo[]) value, jsonWriter); - } else if (value instanceof SharedPreferences) { - JsonUtils.writeJson((SharedPreferences) value, jsonWriter); - } else if (value instanceof Key[]) { - JsonUtils.writeJson((Key[]) value, jsonWriter); - } else if (value instanceof SuggestedWords) { - JsonUtils.writeJson((SuggestedWords) value, jsonWriter); - } else if (value instanceof MotionEvent) { - JsonUtils.writeJson((MotionEvent) value, jsonWriter); - } else if (value == null) { - jsonWriter.nullValue(); - } else { - if (DEBUG) { - Log.w(TAG, "Unrecognized type to be logged: " - + (value == null ? "<null>" : value.getClass().getName())); - } - jsonWriter.nullValue(); - } - } - jsonWriter.endObject(); - } catch (IOException e) { - e.printStackTrace(); - Log.w(TAG, "Error in JsonWriter; skipping LogStatement"); - return false; - } - return true; - } -} diff --git a/java/src/com/android/inputmethod/research/LogUnit.java b/java/src/com/android/inputmethod/research/LogUnit.java deleted file mode 100644 index 3366df12a..000000000 --- a/java/src/com/android/inputmethod/research/LogUnit.java +++ /dev/null @@ -1,496 +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.research; - -import android.os.SystemClock; -import android.text.TextUtils; -import android.util.JsonWriter; -import android.util.Log; - -import com.android.inputmethod.latin.SuggestedWords; -import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; -import com.android.inputmethod.latin.define.ProductionFlag; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.regex.Pattern; - -/** - * 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. - */ -public class LogUnit { - private static final String TAG = LogUnit.class.getSimpleName(); - private static final boolean DEBUG = false - && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; - - private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+"); - private static final String[] EMPTY_STRING_ARRAY = new String[0]; - - private final ArrayList<LogStatement> mLogStatementList; - private final ArrayList<Object[]> mValuesList; - // Assume that mTimeList is sorted in increasing order. Do not insert null values into - // mTimeList. - private final ArrayList<Long> mTimeList; - // Words that this LogUnit generates. Should be null if the data in the LogUnit does not - // generate a genuine word (i.e. separators alone do not count as a word). Should never be - // empty. Note that if the user types spaces explicitly, then normally mWords should contain - // only a single word; it will only contain space-separate multiple words if the user does not - // enter a space, and the system enters one automatically. - private String mWords; - private String[] mWordArray = EMPTY_STRING_ARRAY; - private boolean mMayContainDigit; - private boolean mIsPartOfMegaword; - private boolean mContainsUserDeletions; - - // mCorrectionType indicates whether the word was corrected at all, and if so, the nature of the - // correction. - private int mCorrectionType; - // LogUnits start in this state. If a word is entered without being corrected, it will have - // this CorrectiontType. - public static final int CORRECTIONTYPE_NO_CORRECTION = 0; - // The LogUnit was corrected manually by the user in an unspecified way. - public static final int CORRECTIONTYPE_CORRECTION = 1; - // The LogUnit was corrected manually by the user to a word not in the list of suggestions of - // the first word typed here. (Note: this is a heuristic value, it may be incorrect, for - // example, if the user repositions the cursor). - public static final int CORRECTIONTYPE_DIFFERENT_WORD = 2; - // The LogUnit was corrected manually by the user to a word that was in the list of suggestions - // of the first word typed here. (Again, a heuristic). It is probably a typo correction. - public static final int CORRECTIONTYPE_TYPO = 3; - // TODO: Rather than just tracking the current state, keep a historical record of the LogUnit's - // state and statistics. This should include how many times it has been corrected, whether - // other LogUnit edits were done between edits to this LogUnit, etc. Also track when a LogUnit - // previously contained a word, but was corrected to empty (because it was deleted, and there is - // no known replacement). - - private SuggestedWords mSuggestedWords; - - public LogUnit() { - mLogStatementList = new ArrayList<LogStatement>(); - mValuesList = new ArrayList<Object[]>(); - mTimeList = new ArrayList<Long>(); - mIsPartOfMegaword = false; - mCorrectionType = CORRECTIONTYPE_NO_CORRECTION; - mSuggestedWords = null; - } - - private LogUnit(final ArrayList<LogStatement> logStatementList, - final ArrayList<Object[]> valuesList, - final ArrayList<Long> timeList, - final boolean isPartOfMegaword) { - mLogStatementList = logStatementList; - mValuesList = valuesList; - mTimeList = timeList; - mIsPartOfMegaword = isPartOfMegaword; - mCorrectionType = CORRECTIONTYPE_NO_CORRECTION; - mSuggestedWords = null; - } - - private static final Object[] NULL_VALUES = new Object[0]; - /** - * Adds a new log statement. The time parameter in successive calls to this method must be - * monotonically increasing, or splitByTime() will not work. - */ - public void addLogStatement(final LogStatement logStatement, final long time, - Object... values) { - if (values == null) { - values = NULL_VALUES; - } - mLogStatementList.add(logStatement); - mValuesList.add(values); - mTimeList.add(time); - } - - /** - * Publish the contents of this LogUnit to {@code researchLog}. - * - * For each publishable {@code LogStatement}, invoke {@link LogStatement#outputToLocked}. - * - * @param researchLog where to publish the contents of this {@code LogUnit} - * @param canIncludePrivateData whether the private data in this {@code LogUnit} should be - * included - * - * @throws IOException if publication to the log file is not possible - */ - public synchronized void publishTo(final ResearchLog researchLog, - final boolean canIncludePrivateData) throws IOException { - // Write out any logStatement that passes the privacy filter. - final int size = mLogStatementList.size(); - if (size != 0) { - // Note that jsonWriter is only set to a non-null value if the logUnit start text is - // output and at least one logStatement is output. - JsonWriter jsonWriter = researchLog.getInitializedJsonWriterLocked(); - outputLogUnitStart(jsonWriter, canIncludePrivateData); - for (int i = 0; i < size; i++) { - final LogStatement logStatement = mLogStatementList.get(i); - if (!canIncludePrivateData && logStatement.isPotentiallyPrivate()) { - continue; - } - if (mIsPartOfMegaword && logStatement.isPotentiallyRevealing()) { - continue; - } - logStatement.outputToLocked(jsonWriter, mTimeList.get(i), mValuesList.get(i)); - } - outputLogUnitStop(jsonWriter); - } - } - - private static final String WORD_KEY = "_wo"; - private static final String NUM_WORDS_KEY = "_nw"; - private static final String CORRECTION_TYPE_KEY = "_corType"; - private static final String LOG_UNIT_BEGIN_KEY = "logUnitStart"; - private static final String LOG_UNIT_END_KEY = "logUnitEnd"; - - final LogStatement LOGSTATEMENT_LOG_UNIT_BEGIN_WITH_PRIVATE_DATA = - new LogStatement(LOG_UNIT_BEGIN_KEY, false /* isPotentiallyPrivate */, - false /* isPotentiallyRevealing */, WORD_KEY, CORRECTION_TYPE_KEY, - NUM_WORDS_KEY); - final LogStatement LOGSTATEMENT_LOG_UNIT_BEGIN_WITHOUT_PRIVATE_DATA = - new LogStatement(LOG_UNIT_BEGIN_KEY, false /* isPotentiallyPrivate */, - false /* isPotentiallyRevealing */, NUM_WORDS_KEY); - private void outputLogUnitStart(final JsonWriter jsonWriter, - final boolean canIncludePrivateData) { - final LogStatement logStatement; - if (canIncludePrivateData) { - LOGSTATEMENT_LOG_UNIT_BEGIN_WITH_PRIVATE_DATA.outputToLocked(jsonWriter, - SystemClock.uptimeMillis(), getWordsAsString(), getCorrectionType(), - getNumWords()); - } else { - LOGSTATEMENT_LOG_UNIT_BEGIN_WITHOUT_PRIVATE_DATA.outputToLocked(jsonWriter, - SystemClock.uptimeMillis(), getNumWords()); - } - } - - final LogStatement LOGSTATEMENT_LOG_UNIT_END = - new LogStatement(LOG_UNIT_END_KEY, false /* isPotentiallyPrivate */, - false /* isPotentiallyRevealing */); - private void outputLogUnitStop(final JsonWriter jsonWriter) { - LOGSTATEMENT_LOG_UNIT_END.outputToLocked(jsonWriter, SystemClock.uptimeMillis()); - } - - /** - * Mark the current logUnit as containing data to generate {@code newWords}. - * - * If {@code setWord()} was previously called for this LogUnit, then the method will try to - * determine what kind of correction it is, and update its internal state of the correctionType - * accordingly. - * - * @param newWords The words this LogUnit generates. Caller should not pass null or the empty - * string. - */ - public void setWords(final String newWords) { - if (hasOneOrMoreWords()) { - // The word was already set once, and it is now being changed. See if the new word - // is close to the old word. If so, then the change is probably a typo correction. - // If not, the user may have decided to enter a different word, so flag it. - if (mSuggestedWords != null) { - if (isInSuggestedWords(newWords, mSuggestedWords)) { - mCorrectionType = CORRECTIONTYPE_TYPO; - } else { - mCorrectionType = CORRECTIONTYPE_DIFFERENT_WORD; - } - } else { - // No suggested words, so it's not clear whether it's a typo or different word. - // Mark it as a generic correction. - mCorrectionType = CORRECTIONTYPE_CORRECTION; - } - } else { - mCorrectionType = CORRECTIONTYPE_NO_CORRECTION; - } - mWords = newWords; - - // Update mWordArray - mWordArray = (TextUtils.isEmpty(mWords)) ? EMPTY_STRING_ARRAY - : WHITESPACE_PATTERN.split(mWords); - if (mWordArray.length > 0 && TextUtils.isEmpty(mWordArray[0])) { - // Empty string at beginning of array. Must have been whitespace at the start of the - // word. Remove the empty string. - mWordArray = Arrays.copyOfRange(mWordArray, 1, mWordArray.length); - } - } - - public String getWordsAsString() { - return mWords; - } - - /** - * Retuns the words generated by the data in this LogUnit. - * - * The first word may be an empty string, if the data in the LogUnit started by generating - * whitespace. - * - * @return the array of words. an empty list of there are no words associated with this LogUnit. - */ - public String[] getWordsAsStringArray() { - return mWordArray; - } - - public boolean hasOneOrMoreWords() { - return mWordArray.length >= 1; - } - - public int getNumWords() { - return mWordArray.length; - } - - // TODO: Refactor to eliminate getter/setters - public void setMayContainDigit() { - mMayContainDigit = true; - } - - // TODO: Refactor to eliminate getter/setters - public boolean mayContainDigit() { - return mMayContainDigit; - } - - // TODO: Refactor to eliminate getter/setters - public void setContainsUserDeletions() { - mContainsUserDeletions = true; - } - - // TODO: Refactor to eliminate getter/setters - public boolean containsUserDeletions() { - return mContainsUserDeletions; - } - - // TODO: Refactor to eliminate getter/setters - public void setCorrectionType(final int correctionType) { - mCorrectionType = correctionType; - } - - // TODO: Refactor to eliminate getter/setters - public int getCorrectionType() { - return mCorrectionType; - } - - public boolean isEmpty() { - return mLogStatementList.isEmpty(); - } - - /** - * Split this logUnit, with all events before maxTime staying in the current logUnit, and all - * events after maxTime going into a new LogUnit that is returned. - */ - public LogUnit splitByTime(final long maxTime) { - // Assume that mTimeList is in sorted order. - final int length = mTimeList.size(); - // TODO: find time by binary search, e.g. using Collections#binarySearch() - for (int index = 0; index < length; index++) { - if (mTimeList.get(index) > maxTime) { - final List<LogStatement> laterLogStatements = - mLogStatementList.subList(index, length); - final List<Object[]> laterValues = mValuesList.subList(index, length); - final List<Long> laterTimes = mTimeList.subList(index, length); - - // Create the LogUnit containing the later logStatements and associated data. - final LogUnit newLogUnit = new LogUnit( - new ArrayList<LogStatement>(laterLogStatements), - new ArrayList<Object[]>(laterValues), - new ArrayList<Long>(laterTimes), - true /* isPartOfMegaword */); - newLogUnit.mWords = null; - newLogUnit.mMayContainDigit = mMayContainDigit; - newLogUnit.mContainsUserDeletions = mContainsUserDeletions; - - // Purge the logStatements and associated data from this LogUnit. - laterLogStatements.clear(); - laterValues.clear(); - laterTimes.clear(); - mIsPartOfMegaword = true; - - return newLogUnit; - } - } - return new LogUnit(); - } - - public void append(final LogUnit logUnit) { - mLogStatementList.addAll(logUnit.mLogStatementList); - mValuesList.addAll(logUnit.mValuesList); - mTimeList.addAll(logUnit.mTimeList); - mWords = null; - if (logUnit.mWords != null) { - setWords(logUnit.mWords); - } - mMayContainDigit = mMayContainDigit || logUnit.mMayContainDigit; - mContainsUserDeletions = mContainsUserDeletions || logUnit.mContainsUserDeletions; - mIsPartOfMegaword = false; - } - - public SuggestedWords getSuggestions() { - return mSuggestedWords; - } - - /** - * Initialize the suggestions. - * - * Once set to a non-null value, the suggestions may not be changed again. This is to keep - * track of the list of words that are close to the user's initial effort to type the word. - * Only words that are close to the initial effort are considered typo corrections. - */ - public void initializeSuggestions(final SuggestedWords suggestedWords) { - if (mSuggestedWords == null) { - mSuggestedWords = suggestedWords; - } - } - - private static boolean isInSuggestedWords(final String queryWord, - final SuggestedWords suggestedWords) { - if (TextUtils.isEmpty(queryWord)) { - return false; - } - final int size = suggestedWords.size(); - for (int i = 0; i < size; i++) { - final SuggestedWordInfo wordInfo = suggestedWords.getInfo(i); - if (queryWord.equals(wordInfo.mWord)) { - return true; - } - } - return false; - } - - /** - * Remove data associated with selecting the Research button. - * - * A LogUnit will capture all user interactions with the IME, including the "meta-interactions" - * of using the Research button to control the logging (e.g. by starting and stopping recording - * of a test case). Because meta-interactions should not be part of the normal log, calling - * this method will set a field in the LogStatements of the motion events to indiciate that - * they should be disregarded. - * - * This implementation assumes that the data recorded by the meta-interaction takes the - * form of all events following the first MotionEvent.ACTION_DOWN before the first long-press - * before the last onCodeEvent containing a code matching {@code LogStatement.VALUE_RESEARCH}. - * - * @returns true if data was removed - */ - public boolean removeResearchButtonInvocation() { - // This method is designed to be idempotent. - - // First, find last invocation of "research" key - final int indexOfLastResearchKey = findLastIndexContainingKeyValue( - LogStatement.TYPE_POINTER_TRACKER_CALL_LISTENER_ON_CODE_INPUT, - LogStatement.KEY_CODE, LogStatement.VALUE_RESEARCH); - if (indexOfLastResearchKey < 0) { - // Could not find invocation of "research" key. Leave log as is. - if (DEBUG) { - Log.d(TAG, "Could not find research key"); - } - return false; - } - - // Look for the long press that started the invocation of the research key code input. - final int indexOfLastLongPressBeforeResearchKey = - findLastIndexBefore(LogStatement.TYPE_MAIN_KEYBOARD_VIEW_ON_LONG_PRESS, - indexOfLastResearchKey); - - // Look for DOWN event preceding the long press - final int indexOfLastDownEventBeforeLongPress = - findLastIndexContainingKeyValueBefore(LogStatement.TYPE_MOTION_EVENT, - LogStatement.ACTION, LogStatement.VALUE_DOWN, - indexOfLastLongPressBeforeResearchKey); - - // Flag all LatinKeyboardViewProcessMotionEvents from the DOWN event to the research key as - // logging-related - final int startingIndex = indexOfLastDownEventBeforeLongPress == -1 ? 0 - : indexOfLastDownEventBeforeLongPress; - for (int index = startingIndex; index < indexOfLastResearchKey; index++) { - final LogStatement logStatement = mLogStatementList.get(index); - final String type = logStatement.getType(); - final Object[] values = mValuesList.get(index); - if (type.equals(LogStatement.TYPE_MOTION_EVENT)) { - logStatement.setValue(LogStatement.KEY_IS_LOGGING_RELATED, values, true); - } - } - return true; - } - - /** - * Find the index of the last LogStatement before {@code startingIndex} of type {@code type}. - * - * @param queryType a String that must be {@code String.equals()} to the LogStatement type - * @param startingIndex the index to start the backward search from. Must be less than the - * length of mLogStatementList, or an IndexOutOfBoundsException is thrown. Can be negative, - * in which case -1 is returned. - * - * @return The index of the last LogStatement, -1 if none exists. - */ - private int findLastIndexBefore(final String queryType, final int startingIndex) { - return findLastIndexContainingKeyValueBefore(queryType, null, null, startingIndex); - } - - /** - * Find the index of the last LogStatement before {@code startingIndex} of type {@code type} - * containing the given key-value pair. - * - * @param queryType a String that must be {@code String.equals()} to the LogStatement type - * @param queryKey a String that must be {@code String.equals()} to a key in the LogStatement - * @param queryValue an Object that must be {@code String.equals()} to the key's corresponding - * value - * - * @return The index of the last LogStatement, -1 if none exists. - */ - private int findLastIndexContainingKeyValue(final String queryType, final String queryKey, - final Object queryValue) { - return findLastIndexContainingKeyValueBefore(queryType, queryKey, queryValue, - mLogStatementList.size() - 1); - } - - /** - * Find the index of the last LogStatement before {@code startingIndex} of type {@code type} - * containing the given key-value pair. - * - * @param queryType a String that must be {@code String.equals()} to the LogStatement type - * @param queryKey a String that must be {@code String.equals()} to a key in the LogStatement - * @param queryValue an Object that must be {@code String.equals()} to the key's corresponding - * value - * @param startingIndex the index to start the backward search from. Must be less than the - * length of mLogStatementList, or an IndexOutOfBoundsException is thrown. Can be negative, - * in which case -1 is returned. - * - * @return The index of the last LogStatement, -1 if none exists. - */ - private int findLastIndexContainingKeyValueBefore(final String queryType, final String queryKey, - final Object queryValue, final int startingIndex) { - if (startingIndex < 0) { - return -1; - } - for (int index = startingIndex; index >= 0; index--) { - final LogStatement logStatement = mLogStatementList.get(index); - final String type = logStatement.getType(); - if (type.equals(queryType) && (queryKey == null - || logStatement.containsKeyValuePair(queryKey, queryValue, - mValuesList.get(index)))) { - return index; - } - } - return -1; - } -} diff --git a/java/src/com/android/inputmethod/research/LoggingUtils.java b/java/src/com/android/inputmethod/research/LoggingUtils.java deleted file mode 100644 index 1261d6780..000000000 --- a/java/src/com/android/inputmethod/research/LoggingUtils.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.research; - -import android.view.MotionEvent; - -/* package */ class LoggingUtils { - private LoggingUtils() { - // This utility class is not publicly instantiable. - } - - /* package */ static String getMotionEventActionTypeString(final int actionType) { - switch (actionType) { - case MotionEvent.ACTION_CANCEL: return "CANCEL"; - case MotionEvent.ACTION_UP: return "UP"; - case MotionEvent.ACTION_DOWN: return "DOWN"; - case MotionEvent.ACTION_POINTER_UP: return "POINTER_UP"; - case MotionEvent.ACTION_POINTER_DOWN: return "POINTER_DOWN"; - case MotionEvent.ACTION_MOVE: return "MOVE"; - case MotionEvent.ACTION_OUTSIDE: return "OUTSIDE"; - default: return "ACTION_" + actionType; - } - } -} diff --git a/java/src/com/android/inputmethod/research/MainLogBuffer.java b/java/src/com/android/inputmethod/research/MainLogBuffer.java deleted file mode 100644 index 6df7c1708..000000000 --- a/java/src/com/android/inputmethod/research/MainLogBuffer.java +++ /dev/null @@ -1,287 +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.research; - -import android.util.Log; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.Dictionary; -import com.android.inputmethod.latin.Suggest; -import com.android.inputmethod.latin.define.ProductionFlag; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.LinkedList; - -/** - * MainLogBuffer is a FixedLogBuffer that tracks the state of LogUnits to make privacy guarantees. - * - * There are three forms of privacy protection: 1) only words in the main dictionary are allowed to - * be logged in enough detail to determine their contents, 2) only a subset of words are logged - * in detail, such as 10%, and 3) no numbers are logged. - * - * This class maintains a list of LogUnits, each corresponding to a word. As the user completes - * words, they are added here. But if the user backs up over their current word to edit a word - * entered earlier, then it is pulled out of this LogBuffer, changes are then added to the end of - * the LogUnit, and it is pushed back in here when the user is done. Because words may be pulled - * back out even after they are pushed in, we must not publish the contents of this LogBuffer too - * quickly. However, we cannot let the contents pile up either, or it will limit the editing that - * a user can perform. - * - * To balance these requirements (keep history so user can edit, flush history so it does not pile - * up), the LogBuffer is considered "complete" when the user has entered enough words to form an - * n-gram, followed by enough additional non-detailed words (that are in the 90%, as per above). - * Once complete, the n-gram may be published to flash storage (via the ResearchLog class). - * However, the additional non-detailed words are retained, in case the user backspaces to edit - * them. The MainLogBuffer then continues to add words, publishing individual non-detailed words - * as new words arrive. After enough non-detailed words have been pushed out to account for the - * 90% between words, the words at the front of the LogBuffer can be published as an n-gram again. - * - * If the words that would form the valid n-gram are not in the dictionary, then words are pushed - * through the LogBuffer one at a time until an n-gram is found that is entirely composed of - * dictionary words. - * - * If the user closes a session, then the entire LogBuffer is flushed, publishing any embedded - * n-gram containing dictionary words. - */ -public abstract class MainLogBuffer extends FixedLogBuffer { - private static final String TAG = MainLogBuffer.class.getSimpleName(); - private static final boolean DEBUG = false - && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; - - // Keep consistent with switch statement in Statistics.recordPublishabilityResultCode() - public static final int PUBLISHABILITY_PUBLISHABLE = 0; - public static final int PUBLISHABILITY_UNPUBLISHABLE_STOPPING = 1; - public static final int PUBLISHABILITY_UNPUBLISHABLE_INCORRECT_WORD_COUNT = 2; - public static final int PUBLISHABILITY_UNPUBLISHABLE_SAMPLED_TOO_RECENTLY = 3; - public static final int PUBLISHABILITY_UNPUBLISHABLE_DICTIONARY_UNAVAILABLE = 4; - public static final int PUBLISHABILITY_UNPUBLISHABLE_MAY_CONTAIN_DIGIT = 5; - public static final int PUBLISHABILITY_UNPUBLISHABLE_NOT_IN_DICTIONARY = 6; - - // The size of the n-grams logged. E.g. N_GRAM_SIZE = 2 means to sample bigrams. - public static final int N_GRAM_SIZE = 2; - - // TODO: Remove dependence on Suggest, and pass in Dictionary as a parameter to an appropriate - // method. - private final Suggest mSuggest; - @UsedForTesting - private Dictionary mDictionaryForTesting; - private boolean mIsStopping = false; - - /* package for test */ int mNumWordsBetweenNGrams; - - // 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 mNumWordsUntilSafeToSample; - - public MainLogBuffer(final int wordsBetweenSamples, final int numInitialWordsToIgnore, - final Suggest suggest) { - super(N_GRAM_SIZE + wordsBetweenSamples); - mNumWordsBetweenNGrams = wordsBetweenSamples; - mNumWordsUntilSafeToSample = DEBUG ? 0 : numInitialWordsToIgnore; - mSuggest = suggest; - } - - @UsedForTesting - /* package for test */ void setDictionaryForTesting(final Dictionary dictionary) { - mDictionaryForTesting = dictionary; - } - - private Dictionary getDictionary() { - if (mDictionaryForTesting != null) { - return mDictionaryForTesting; - } - if (mSuggest == null || !mSuggest.hasMainDictionary()) return null; - return mSuggest.getMainDictionary(); - } - - public void setIsStopping() { - mIsStopping = true; - } - - /** - * Determines whether the string determined by a series of LogUnits will not violate user - * privacy if published. - * - * @param logUnits a LogUnit list to check for publishability - * @param nGramSize the smallest n-gram acceptable to be published. if - * {@link ResearchLogger#IS_LOGGING_EVERYTHING} is true, then publish if there are more than - * {@code minNGramSize} words in the logUnits, otherwise wait. if {@link - * ResearchLogger#IS_LOGGING_EVERYTHING} is false, then ensure that there are exactly nGramSize - * words in the LogUnits. - * - * @return one of the {@code PUBLISHABILITY_*} result codes defined in this class. - */ - private int getPublishabilityResultCode(final ArrayList<LogUnit> logUnits, - final int nGramSize) { - // Bypass privacy checks when debugging. - if (ResearchLogger.IS_LOGGING_EVERYTHING) { - if (mIsStopping) { - return PUBLISHABILITY_UNPUBLISHABLE_STOPPING; - } - // Only check that it is the right length. If not, wait for later words to make - // complete n-grams. - int numWordsInLogUnitList = 0; - final int length = logUnits.size(); - for (int i = 0; i < length; i++) { - final LogUnit logUnit = logUnits.get(i); - numWordsInLogUnitList += logUnit.getNumWords(); - } - if (numWordsInLogUnitList >= nGramSize) { - return PUBLISHABILITY_PUBLISHABLE; - } else { - return PUBLISHABILITY_UNPUBLISHABLE_INCORRECT_WORD_COUNT; - } - } - - // Check that we are not sampling too frequently. Having sampled recently might disclose - // too much of the user's intended meaning. - if (mNumWordsUntilSafeToSample > 0) { - return PUBLISHABILITY_UNPUBLISHABLE_SAMPLED_TOO_RECENTLY; - } - // Reload the dictionary in case it has changed (e.g., because the user has changed - // languages). - final Dictionary dictionary = getDictionary(); - if (dictionary == null) { - // Main dictionary is unavailable. Since we cannot check it, we cannot tell if a - // word is out-of-vocabulary or not. Therefore, we must judge the entire buffer - // contents to potentially pose a privacy risk. - return PUBLISHABILITY_UNPUBLISHABLE_DICTIONARY_UNAVAILABLE; - } - - // Check each word in the buffer. If any word poses a privacy threat, we cannot upload - // the complete buffer contents in detail. - int numWordsInLogUnitList = 0; - final int length = logUnits.size(); - for (final LogUnit logUnit : logUnits) { - if (!logUnit.hasOneOrMoreWords()) { - // Digits outside words are a privacy threat. - if (logUnit.mayContainDigit()) { - return PUBLISHABILITY_UNPUBLISHABLE_MAY_CONTAIN_DIGIT; - } - } else { - numWordsInLogUnitList += logUnit.getNumWords(); - final String[] words = logUnit.getWordsAsStringArray(); - for (final String word : words) { - // Words not in the dictionary are a privacy threat. - if (ResearchLogger.hasLetters(word) && !(dictionary.isValidWord(word))) { - if (DEBUG) { - Log.d(TAG, "\"" + word + "\" NOT SAFE!: hasLetters: " - + ResearchLogger.hasLetters(word) - + ", isValid: " + (dictionary.isValidWord(word))); - } - return PUBLISHABILITY_UNPUBLISHABLE_NOT_IN_DICTIONARY; - } - } - } - } - - // Finally, only return true if the ngram is the right size. - if (numWordsInLogUnitList == nGramSize) { - return PUBLISHABILITY_PUBLISHABLE; - } else { - return PUBLISHABILITY_UNPUBLISHABLE_INCORRECT_WORD_COUNT; - } - } - - public void shiftAndPublishAll() throws IOException { - final LinkedList<LogUnit> logUnits = getLogUnits(); - while (!logUnits.isEmpty()) { - publishLogUnitsAtFrontOfBuffer(); - } - } - - @Override - protected final void onBufferFull() { - try { - publishLogUnitsAtFrontOfBuffer(); - } catch (final IOException e) { - if (DEBUG) { - Log.w(TAG, "IOException when publishing front of LogBuffer", e); - } - } - } - - /** - * If there is a safe n-gram at the front of this log buffer, publish it with all details, and - * remove the LogUnits that constitute it. - * - * An n-gram might not be "safe" if it violates privacy controls. E.g., it might contain - * numbers, an out-of-vocabulary word, or another n-gram may have been published recently. If - * there is no safe n-gram, then the LogUnits up through the first word-containing LogUnit are - * published, but without disclosing any privacy-related details, such as the word the LogUnit - * generated, motion data, etc. - * - * Note that a LogUnit can hold more than one word if the user types without explicit spaces. - * In this case, the words may be grouped together in such a way that pulling an n-gram off the - * front would require splitting a LogUnit. Splitting a LogUnit is not possible, so this case - * is treated just as the unsafe n-gram case. This may cause n-grams to be sampled at slightly - * less than the target frequency. - */ - protected final void publishLogUnitsAtFrontOfBuffer() throws IOException { - // TODO: Refactor this method to require fewer passes through the LogUnits. Should really - // require only one pass. - ArrayList<LogUnit> logUnits = peekAtFirstNWords(N_GRAM_SIZE); - final int publishabilityResultCode = getPublishabilityResultCode(logUnits, N_GRAM_SIZE); - ResearchLogger.recordPublishabilityResultCode(publishabilityResultCode); - if (publishabilityResultCode == MainLogBuffer.PUBLISHABILITY_PUBLISHABLE) { - // Good n-gram at the front of the buffer. Publish it, disclosing details. - publish(logUnits, true /* canIncludePrivateData */); - shiftOutWords(N_GRAM_SIZE); - mNumWordsUntilSafeToSample = mNumWordsBetweenNGrams; - return; - } - // No good n-gram at front, and buffer is full. Shift out up through the first logUnit - // with associated words (or if there is none, all the existing logUnits). - logUnits.clear(); - LogUnit logUnit = shiftOut(); - while (logUnit != null) { - logUnits.add(logUnit); - final int numWords = logUnit.getNumWords(); - if (numWords > 0) { - mNumWordsUntilSafeToSample = Math.max(0, mNumWordsUntilSafeToSample - numWords); - break; - } - logUnit = shiftOut(); - } - publish(logUnits, false /* canIncludePrivateData */); - } - - /** - * Called when a list of logUnits should be published. - * - * It is the subclass's responsibility to implement the publication. - * - * @param logUnits The list of logUnits to be published. - * @param canIncludePrivateData Whether the private data in the logUnits can be included in - * publication. - * - * @throws IOException if publication to the log file is not possible - */ - protected abstract void publish(final ArrayList<LogUnit> logUnits, - final boolean canIncludePrivateData) throws IOException; - - @Override - protected int shiftOutWords(final int numWords) { - final int numWordsShiftedOut = super.shiftOutWords(numWords); - mNumWordsUntilSafeToSample = Math.max(0, mNumWordsUntilSafeToSample - numWordsShiftedOut); - if (DEBUG) { - Log.d(TAG, "wordsUntilSafeToSample now at " + mNumWordsUntilSafeToSample); - } - return numWordsShiftedOut; - } -} diff --git a/java/src/com/android/inputmethod/research/MotionEventReader.java b/java/src/com/android/inputmethod/research/MotionEventReader.java deleted file mode 100644 index 3388645b7..000000000 --- a/java/src/com/android/inputmethod/research/MotionEventReader.java +++ /dev/null @@ -1,326 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.android.inputmethod.research; - -import android.util.JsonReader; -import android.util.Log; -import android.view.MotionEvent; -import android.view.MotionEvent.PointerCoords; -import android.view.MotionEvent.PointerProperties; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.define.ProductionFlag; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.ArrayList; - -public class MotionEventReader { - private static final String TAG = MotionEventReader.class.getSimpleName(); - private static final boolean DEBUG = false - && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; - // Assumes that MotionEvent.ACTION_MASK does not have all bits set.` - private static final int UNINITIALIZED_ACTION = ~MotionEvent.ACTION_MASK; - // No legitimate int is negative - private static final int UNINITIALIZED_INT = -1; - // No legitimate long is negative - private static final long UNINITIALIZED_LONG = -1L; - // No legitimate float is negative - private static final float UNINITIALIZED_FLOAT = -1.0f; - - public ReplayData readMotionEventData(final File file) { - final ReplayData replayData = new ReplayData(); - try { - // Read file - final JsonReader jsonReader = new JsonReader(new BufferedReader(new InputStreamReader( - new FileInputStream(file)))); - jsonReader.beginArray(); - while (jsonReader.hasNext()) { - readLogStatement(jsonReader, replayData); - } - jsonReader.endArray(); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - return replayData; - } - - @UsedForTesting - static class ReplayData { - final ArrayList<Integer> mActions = new ArrayList<Integer>(); - final ArrayList<PointerProperties[]> mPointerPropertiesArrays - = new ArrayList<PointerProperties[]>(); - final ArrayList<PointerCoords[]> mPointerCoordsArrays = new ArrayList<PointerCoords[]>(); - final ArrayList<Long> mTimes = new ArrayList<Long>(); - } - - /** - * Read motion data from a logStatement and store it in {@code replayData}. - * - * Two kinds of logStatements can be read. In the first variant, the MotionEvent data is - * represented as attributes at the top level like so: - * - * <pre> - * { - * "_ct": 1359590400000, - * "_ut": 4381933, - * "_ty": "MotionEvent", - * "action": "UP", - * "isLoggingRelated": false, - * "x": 100, - * "y": 200 - * } - * </pre> - * - * In the second variant, there is a separate attribute for the MotionEvent that includes - * historical data if present: - * - * <pre> - * { - * "_ct": 135959040000, - * "_ut": 4382702, - * "_ty": "MotionEvent", - * "action": "MOVE", - * "isLoggingRelated": false, - * "motionEvent": { - * "pointerIds": [ - * 0 - * ], - * "xyt": [ - * { - * "t": 4382551, - * "d": [ - * { - * "x": 141.25, - * "y": 151.8485107421875, - * "toma": 101.82337188720703, - * "tomi": 101.82337188720703, - * "o": 0.0 - * } - * ] - * }, - * { - * "t": 4382559, - * "d": [ - * { - * "x": 140.7266082763672, - * "y": 151.8485107421875, - * "toma": 101.82337188720703, - * "tomi": 101.82337188720703, - * "o": 0.0 - * } - * ] - * } - * ] - * } - * }, - * </pre> - */ - @UsedForTesting - /* package for test */ void readLogStatement(final JsonReader jsonReader, - final ReplayData replayData) throws IOException { - String logStatementType = null; - int actionType = UNINITIALIZED_ACTION; - int x = UNINITIALIZED_INT; - int y = UNINITIALIZED_INT; - long time = UNINITIALIZED_LONG; - boolean isLoggingRelated = false; - - jsonReader.beginObject(); - while (jsonReader.hasNext()) { - final String key = jsonReader.nextName(); - if (key.equals("_ty")) { - logStatementType = jsonReader.nextString(); - } else if (key.equals("_ut")) { - time = jsonReader.nextLong(); - } else if (key.equals("x")) { - x = jsonReader.nextInt(); - } else if (key.equals("y")) { - y = jsonReader.nextInt(); - } else if (key.equals("action")) { - final String s = jsonReader.nextString(); - if (s.equals("UP")) { - actionType = MotionEvent.ACTION_UP; - } else if (s.equals("DOWN")) { - actionType = MotionEvent.ACTION_DOWN; - } else if (s.equals("MOVE")) { - actionType = MotionEvent.ACTION_MOVE; - } - } else if (key.equals("loggingRelated")) { - isLoggingRelated = jsonReader.nextBoolean(); - } else if (logStatementType != null && logStatementType.equals("MotionEvent") - && key.equals("motionEvent")) { - if (actionType == UNINITIALIZED_ACTION) { - Log.e(TAG, "no actionType assigned in MotionEvent json"); - } - // Second variant of LogStatement. - if (isLoggingRelated) { - jsonReader.skipValue(); - } else { - readEmbeddedMotionEvent(jsonReader, replayData, actionType); - } - } else { - if (DEBUG) { - Log.w(TAG, "Unknown JSON key in LogStatement: " + key); - } - jsonReader.skipValue(); - } - } - jsonReader.endObject(); - - if (logStatementType != null && time != UNINITIALIZED_LONG && x != UNINITIALIZED_INT - && y != UNINITIALIZED_INT && actionType != UNINITIALIZED_ACTION - && logStatementType.equals("MotionEvent") && !isLoggingRelated) { - // First variant of LogStatement. - final PointerProperties pointerProperties = new PointerProperties(); - pointerProperties.id = 0; - pointerProperties.toolType = MotionEvent.TOOL_TYPE_UNKNOWN; - final PointerProperties[] pointerPropertiesArray = { - pointerProperties - }; - final PointerCoords pointerCoords = new PointerCoords(); - pointerCoords.x = x; - pointerCoords.y = y; - pointerCoords.pressure = 1.0f; - pointerCoords.size = 1.0f; - final PointerCoords[] pointerCoordsArray = { - pointerCoords - }; - addMotionEventData(replayData, actionType, time, pointerPropertiesArray, - pointerCoordsArray); - } - } - - private void readEmbeddedMotionEvent(final JsonReader jsonReader, final ReplayData replayData, - final int actionType) throws IOException { - jsonReader.beginObject(); - PointerProperties[] pointerPropertiesArray = null; - while (jsonReader.hasNext()) { // pointerIds/xyt - final String name = jsonReader.nextName(); - if (name.equals("pointerIds")) { - pointerPropertiesArray = readPointerProperties(jsonReader); - } else if (name.equals("xyt")) { - readPointerData(jsonReader, replayData, actionType, pointerPropertiesArray); - } - } - jsonReader.endObject(); - } - - private PointerProperties[] readPointerProperties(final JsonReader jsonReader) - throws IOException { - final ArrayList<PointerProperties> pointerPropertiesArrayList = - new ArrayList<PointerProperties>(); - jsonReader.beginArray(); - while (jsonReader.hasNext()) { - final PointerProperties pointerProperties = new PointerProperties(); - pointerProperties.id = jsonReader.nextInt(); - pointerProperties.toolType = MotionEvent.TOOL_TYPE_UNKNOWN; - pointerPropertiesArrayList.add(pointerProperties); - } - jsonReader.endArray(); - return pointerPropertiesArrayList.toArray( - new PointerProperties[pointerPropertiesArrayList.size()]); - } - - private void readPointerData(final JsonReader jsonReader, final ReplayData replayData, - final int actionType, final PointerProperties[] pointerPropertiesArray) - throws IOException { - if (pointerPropertiesArray == null) { - Log.e(TAG, "PointerIDs must be given before xyt data in json for MotionEvent"); - jsonReader.skipValue(); - return; - } - long time = UNINITIALIZED_LONG; - jsonReader.beginArray(); - while (jsonReader.hasNext()) { // Array of historical data - jsonReader.beginObject(); - final ArrayList<PointerCoords> pointerCoordsArrayList = new ArrayList<PointerCoords>(); - while (jsonReader.hasNext()) { // Time/data object - final String name = jsonReader.nextName(); - if (name.equals("t")) { - time = jsonReader.nextLong(); - } else if (name.equals("d")) { - jsonReader.beginArray(); - while (jsonReader.hasNext()) { // array of data per pointer - final PointerCoords pointerCoords = readPointerCoords(jsonReader); - if (pointerCoords != null) { - pointerCoordsArrayList.add(pointerCoords); - } - } - jsonReader.endArray(); - } else { - jsonReader.skipValue(); - } - } - jsonReader.endObject(); - // Data was recorded as historical events, but must be split apart into - // separate MotionEvents for replaying - if (time != UNINITIALIZED_LONG) { - addMotionEventData(replayData, actionType, time, pointerPropertiesArray, - pointerCoordsArrayList.toArray( - new PointerCoords[pointerCoordsArrayList.size()])); - } else { - Log.e(TAG, "Time not assigned in json for MotionEvent"); - } - } - jsonReader.endArray(); - } - - private PointerCoords readPointerCoords(final JsonReader jsonReader) throws IOException { - jsonReader.beginObject(); - float x = UNINITIALIZED_FLOAT; - float y = UNINITIALIZED_FLOAT; - while (jsonReader.hasNext()) { // x,y - final String name = jsonReader.nextName(); - if (name.equals("x")) { - x = (float) jsonReader.nextDouble(); - } else if (name.equals("y")) { - y = (float) jsonReader.nextDouble(); - } else { - jsonReader.skipValue(); - } - } - jsonReader.endObject(); - - if (Float.compare(x, UNINITIALIZED_FLOAT) == 0 - || Float.compare(y, UNINITIALIZED_FLOAT) == 0) { - Log.w(TAG, "missing x or y value in MotionEvent json"); - return null; - } - final PointerCoords pointerCoords = new PointerCoords(); - pointerCoords.x = x; - pointerCoords.y = y; - pointerCoords.pressure = 1.0f; - pointerCoords.size = 1.0f; - return pointerCoords; - } - - private void addMotionEventData(final ReplayData replayData, final int actionType, - final long time, final PointerProperties[] pointerProperties, - final PointerCoords[] pointerCoords) { - replayData.mActions.add(actionType); - replayData.mTimes.add(time); - replayData.mPointerPropertiesArrays.add(pointerProperties); - replayData.mPointerCoordsArrays.add(pointerCoords); - } -} diff --git a/java/src/com/android/inputmethod/research/Replayer.java b/java/src/com/android/inputmethod/research/Replayer.java deleted file mode 100644 index 903875f3c..000000000 --- a/java/src/com/android/inputmethod/research/Replayer.java +++ /dev/null @@ -1,150 +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.research; - -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.os.SystemClock; -import android.util.Log; -import android.view.MotionEvent; -import android.view.MotionEvent.PointerCoords; -import android.view.MotionEvent.PointerProperties; - -import com.android.inputmethod.keyboard.KeyboardSwitcher; -import com.android.inputmethod.keyboard.MainKeyboardView; -import com.android.inputmethod.latin.define.ProductionFlag; -import com.android.inputmethod.research.MotionEventReader.ReplayData; - -/** - * Replays a sequence of motion events in realtime on the screen. - * - * Useful for user inspection of logged data. - */ -public class Replayer { - private static final String TAG = Replayer.class.getSimpleName(); - private static final boolean DEBUG = false - && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; - private static final long START_TIME_DELAY_MS = 500; - - private boolean mIsReplaying = false; - private KeyboardSwitcher mKeyboardSwitcher; - - private Replayer() { - } - - private static final Replayer sInstance = new Replayer(); - public static Replayer getInstance() { - return sInstance; - } - - public void setKeyboardSwitcher(final KeyboardSwitcher keyboardSwitcher) { - mKeyboardSwitcher = keyboardSwitcher; - } - - private static final int MSG_MOTION_EVENT = 0; - private static final int MSG_DONE = 1; - private static final int COMPLETION_TIME_MS = 500; - - // TODO: Support historical events and multi-touch. - public void replay(final ReplayData replayData, final Runnable callback) { - if (mIsReplaying) { - return; - } - mIsReplaying = true; - final int numActions = replayData.mActions.size(); - if (DEBUG) { - Log.d(TAG, "replaying " + numActions + " actions"); - } - if (numActions == 0) { - mIsReplaying = false; - return; - } - final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); - - // The reference time relative to the times stored in events. - final long origStartTime = replayData.mTimes.get(0); - // The reference time relative to which events are replayed in the present. - final long currentStartTime = SystemClock.uptimeMillis() + START_TIME_DELAY_MS; - // The adjustment needed to translate times from the original recorded time to the current - // time. - final long timeAdjustment = currentStartTime - origStartTime; - final Handler handler = new Handler(Looper.getMainLooper()) { - // Track the time of the most recent DOWN event, to be passed as a parameter when - // constructing a MotionEvent. It's initialized here to the origStartTime, but this is - // only a precaution. The value should be overwritten by the first ACTION_DOWN event - // before the first use of the variable. Note that this may cause the first few events - // to have incorrect {@code downTime}s. - private long mOrigDownTime = origStartTime; - - @Override - public void handleMessage(final Message msg) { - switch (msg.what) { - case MSG_MOTION_EVENT: - final int index = msg.arg1; - final int action = replayData.mActions.get(index); - final PointerProperties[] pointerPropertiesArray = - replayData.mPointerPropertiesArrays.get(index); - final PointerCoords[] pointerCoordsArray = - replayData.mPointerCoordsArrays.get(index); - final long origTime = replayData.mTimes.get(index); - if (action == MotionEvent.ACTION_DOWN) { - mOrigDownTime = origTime; - } - - final MotionEvent me = MotionEvent.obtain(mOrigDownTime + timeAdjustment, - origTime + timeAdjustment, action, - pointerPropertiesArray.length, pointerPropertiesArray, - pointerCoordsArray, 0, 0, 1.0f, 1.0f, 0, 0, 0, 0); - mainKeyboardView.processMotionEvent(me); - me.recycle(); - break; - case MSG_DONE: - mIsReplaying = false; - ResearchLogger.getInstance().requestIndicatorRedraw(); - break; - } - } - }; - - handler.post(new Runnable() { - @Override - public void run() { - ResearchLogger.getInstance().requestIndicatorRedraw(); - } - }); - for (int i = 0; i < numActions; i++) { - final Message msg = Message.obtain(handler, MSG_MOTION_EVENT, i, 0); - final long msgTime = replayData.mTimes.get(i) + timeAdjustment; - handler.sendMessageAtTime(msg, msgTime); - if (DEBUG) { - Log.d(TAG, "queuing event at " + msgTime); - } - } - - final long presentDoneTime = replayData.mTimes.get(numActions - 1) + timeAdjustment - + COMPLETION_TIME_MS; - handler.sendMessageAtTime(Message.obtain(handler, MSG_DONE), presentDoneTime); - if (callback != null) { - handler.postAtTime(callback, presentDoneTime + 1); - } - } - - public boolean isReplaying() { - return mIsReplaying; - } -} diff --git a/java/src/com/android/inputmethod/research/ReplayerService.java b/java/src/com/android/inputmethod/research/ReplayerService.java deleted file mode 100644 index 88d9033cf..000000000 --- a/java/src/com/android/inputmethod/research/ReplayerService.java +++ /dev/null @@ -1,65 +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.research; - -import android.app.IntentService; -import android.content.Intent; -import android.util.Log; - -import com.android.inputmethod.research.MotionEventReader.ReplayData; - -import java.io.File; -import java.util.concurrent.TimeUnit; - -/** - * Provide a mechanism to invoke the replayer from outside. - * - * In particular, makes access from a host possible through {@code adb am startservice}. - */ -public class ReplayerService extends IntentService { - private static final String TAG = ReplayerService.class.getSimpleName(); - private static final String EXTRA_FILENAME = "com.android.inputmethod.research.extra.FILENAME"; - private static final long MAX_REPLAY_TIME = TimeUnit.SECONDS.toMillis(60); - - public ReplayerService() { - super(ReplayerService.class.getSimpleName()); - } - - @Override - protected void onHandleIntent(final Intent intent) { - final String filename = intent.getStringExtra(EXTRA_FILENAME); - if (filename == null) return; - - final ReplayData replayData = new MotionEventReader().readMotionEventData( - new File(filename)); - synchronized (this) { - Replayer.getInstance().replay(replayData, new Runnable() { - @Override - public void run() { - synchronized (ReplayerService.this) { - ReplayerService.this.notify(); - } - } - }); - try { - wait(MAX_REPLAY_TIME); - } catch (InterruptedException e) { - Log.e(TAG, "Timeout while replaying.", e); - } - } - } -} diff --git a/java/src/com/android/inputmethod/research/ResearchLog.java b/java/src/com/android/inputmethod/research/ResearchLog.java deleted file mode 100644 index 46e620ae5..000000000 --- a/java/src/com/android/inputmethod/research/ResearchLog.java +++ /dev/null @@ -1,298 +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.research; - -import android.content.Context; -import android.util.JsonWriter; -import android.util.Log; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.define.ProductionFlag; - -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.OutputStreamWriter; -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 - * written to a {@link JsonWriter}, which will write to a local file. - * - * The JsonWriter is created on-demand by calling {@link #getInitializedJsonWriterLocked}. - * - * This class uses an executor to perform file-writing operations on a separate thread. It also - * tries to avoid creating unnecessary files if there is nothing to write. It also handles - * flushing, making sure it happens, but not too frequently. - * - * This functionality is off by default. See - * {@link ProductionFlag#USES_DEVELOPMENT_ONLY_DIAGNOSTICS}. - */ -public class ResearchLog { - // TODO: Automatically initialize the JsonWriter rather than requiring the caller to manage it. - private static final String TAG = ResearchLog.class.getSimpleName(); - private static final boolean DEBUG = false - && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; - private static final long FLUSH_DELAY_IN_MS = TimeUnit.SECONDS.toMillis(5); - - /* package */ final ScheduledExecutorService mExecutor; - /* package */ final File mFile; - private final Context mContext; - - // Earlier implementations used a dummy JsonWriter that just swallowed what it was given, but - // this was tricky to do well, because JsonWriter throws an exception if it is passed more than - // one top-level object. - private JsonWriter mJsonWriter = null; - - // 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; - - public ResearchLog(final File outputFile, final Context context) { - mExecutor = Executors.newSingleThreadScheduledExecutor(); - mFile = outputFile; - mContext = context; - } - - /** - * Returns true if this is a FeedbackLog. - * - * FeedbackLogs record only the data associated with a Feedback dialog. Instead of normal - * logging, they contain a LogStatement with the complete feedback string and optionally a - * recording of the user's supplied demo of the problem. - */ - public boolean isFeedbackLog() { - return false; - } - - /** - * Waits for any publication requests to finish and closes the {@link JsonWriter} used for - * output. - * - * See class comment for details about {@code JsonWriter} construction. - * - * @param onClosed run after the close() operation has completed asynchronously - */ - private synchronized void close(final Runnable onClosed) { - mExecutor.submit(new Callable<Object>() { - @Override - public Object call() throws Exception { - try { - if (mJsonWriter == null) return null; - // TODO: This is necessary to avoid an exception. Better would be to not even - // open the JsonWriter if the file is not even opened unless there is valid data - // to write. - if (!mHasWrittenData) { - mJsonWriter.beginArray(); - } - mJsonWriter.endArray(); - mHasWrittenData = false; - mJsonWriter.flush(); - mJsonWriter.close(); - if (DEBUG) { - Log.d(TAG, "closed " + mFile); - } - } catch (final Exception e) { - Log.d(TAG, "error when closing ResearchLog:", e); - } finally { - // Marking the file as read-only signals that this log file is ready to be - // uploaded. - if (mFile != null && mFile.exists()) { - mFile.setWritable(false, false); - } - if (onClosed != null) { - onClosed.run(); - } - } - return null; - } - }); - removeAnyScheduledFlush(); - mExecutor.shutdown(); - } - - /** - * Block until the research log has shut down and spooled out all output or {@code timeout} - * occurs. - * - * @param timeout time to wait for close in milliseconds - */ - public void blockingClose(final long timeout) { - close(null); - awaitTermination(timeout, TimeUnit.MILLISECONDS); - } - - /** - * Waits for publication requests to finish, closes the JsonWriter, but then deletes the backing - * output file. - * - * @param onAbort run after the abort() operation has completed asynchronously - */ - private synchronized void abort(final Runnable onAbort) { - mExecutor.submit(new Callable<Object>() { - @Override - public Object call() throws Exception { - try { - if (mJsonWriter == null) return null; - if (mHasWrittenData) { - // TODO: This is necessary to avoid an exception. Better would be to not - // even open the JsonWriter if the file is not even opened unless there is - // valid data to write. - if (!mHasWrittenData) { - mJsonWriter.beginArray(); - } - mJsonWriter.endArray(); - mJsonWriter.close(); - mHasWrittenData = false; - } - } finally { - if (mFile != null) { - mFile.delete(); - } - if (onAbort != null) { - onAbort.run(); - } - } - return null; - } - }); - removeAnyScheduledFlush(); - mExecutor.shutdown(); - } - - /** - * Block until the research log has aborted or {@code timeout} occurs. - * - * @param timeout time to wait for close in milliseconds - */ - public void blockingAbort(final long timeout) { - abort(null); - awaitTermination(timeout, TimeUnit.MILLISECONDS); - } - - @UsedForTesting - public void awaitTermination(final long delay, final TimeUnit timeUnit) { - try { - if (!mExecutor.awaitTermination(delay, timeUnit)) { - Log.e(TAG, "ResearchLog executor timed out while awaiting terminaion"); - } - } catch (final InterruptedException e) { - Log.e(TAG, "ResearchLog executor interrupted while awaiting terminaion", e); - } - } - - /* package */ synchronized void flush() { - removeAnyScheduledFlush(); - mExecutor.submit(mFlushCallable); - } - - private final Callable<Object> mFlushCallable = new Callable<Object>() { - @Override - public Object call() throws Exception { - if (mJsonWriter != null) 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); - } - - /** - * Queues up {@code logUnit} to be published in the background. - * - * @param logUnit the {@link LogUnit} to be published - * @param canIncludePrivateData whether private data in the LogUnit should be included - */ - public synchronized void publish(final LogUnit logUnit, final boolean canIncludePrivateData) { - try { - mExecutor.submit(new Callable<Object>() { - @Override - public Object call() throws Exception { - logUnit.publishTo(ResearchLog.this, canIncludePrivateData); - scheduleFlush(); - return null; - } - }); - } catch (final RejectedExecutionException e) { - // TODO: Add code to record loss of data, and report. - if (DEBUG) { - Log.d(TAG, "ResearchLog.publish() rejecting scheduled execution", e); - } - } - } - - /** - * Return a JsonWriter for this ResearchLog. It is initialized the first time this method is - * called. The cached value is returned in future calls. - * - * @throws IOException if opening the JsonWriter is not possible - */ - public JsonWriter getInitializedJsonWriterLocked() throws IOException { - if (mJsonWriter != null) return mJsonWriter; - if (mFile == null) throw new FileNotFoundException(); - try { - final JsonWriter jsonWriter = createJsonWriter(mContext, mFile); - if (jsonWriter == null) throw new IOException("Could not create JsonWriter"); - - jsonWriter.beginArray(); - mJsonWriter = jsonWriter; - mHasWrittenData = true; - return mJsonWriter; - } catch (final IOException e) { - if (DEBUG) { - Log.w(TAG, "Exception when creating JsonWriter", e); - Log.w(TAG, "Closing JsonWriter"); - } - if (mJsonWriter != null) mJsonWriter.close(); - mJsonWriter = null; - throw e; - } - } - - /** - * Create the JsonWriter to write the ResearchLog to. - * - * This method may be overriden in testing to redirect the output. - */ - /* package for test */ JsonWriter createJsonWriter(final Context context, final File file) - throws IOException { - return new JsonWriter(new BufferedWriter(new OutputStreamWriter( - context.openFileOutput(file.getName(), Context.MODE_PRIVATE)))); - } -} diff --git a/java/src/com/android/inputmethod/research/ResearchLogDirectory.java b/java/src/com/android/inputmethod/research/ResearchLogDirectory.java deleted file mode 100644 index d156068d6..000000000 --- a/java/src/com/android/inputmethod/research/ResearchLogDirectory.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.research; - -import android.content.Context; -import android.util.Log; - -import java.io.File; -import java.io.FileFilter; - -/** - * Manages log files. - * - * This class handles all aspects where and how research log data is stored. This includes - * generating log filenames in the correct place with the correct names, and cleaning up log files - * under this directory. - */ -public class ResearchLogDirectory { - public static final String TAG = ResearchLogDirectory.class.getSimpleName(); - /* package */ static final String LOG_FILENAME_PREFIX = "researchLog"; - private static final String FILENAME_SUFFIX = ".txt"; - private static final String USER_RECORDING_FILENAME_PREFIX = "recording"; - - private static final ReadOnlyLogFileFilter sUploadableLogFileFilter = - new ReadOnlyLogFileFilter(); - - private final File mFilesDir; - - static class ReadOnlyLogFileFilter implements FileFilter { - @Override - public boolean accept(final File pathname) { - return pathname.getName().startsWith(ResearchLogDirectory.LOG_FILENAME_PREFIX) - && !pathname.canWrite(); - } - } - - /** - * Creates a new ResearchLogDirectory, creating the storage directory if it does not exist. - */ - public ResearchLogDirectory(final Context context) { - mFilesDir = getLoggingDirectory(context); - if (mFilesDir == null) { - throw new NullPointerException("No files directory specified"); - } - if (!mFilesDir.exists()) { - mFilesDir.mkdirs(); - } - } - - private File getLoggingDirectory(final Context context) { - // TODO: Switch to using a subdirectory of getFilesDir(). - return context.getFilesDir(); - } - - /** - * Get an array of log files that are ready for uploading. - * - * A file is ready for uploading if it is marked as read-only. - * - * @return the array of uploadable files - */ - public File[] getUploadableLogFiles() { - try { - return mFilesDir.listFiles(sUploadableLogFileFilter); - } catch (final SecurityException e) { - Log.e(TAG, "Could not cleanup log directory, permission denied", e); - return new File[0]; - } - } - - public void cleanupLogFilesOlderThan(final long time) { - try { - for (final File file : mFilesDir.listFiles()) { - final String filename = file.getName(); - if ((filename.startsWith(LOG_FILENAME_PREFIX) - || filename.startsWith(USER_RECORDING_FILENAME_PREFIX)) - && (file.lastModified() < time)) { - file.delete(); - } - } - } catch (final SecurityException e) { - Log.e(TAG, "Could not cleanup log directory, permission denied", e); - } - } - - public File getLogFilePath(final long time, final long nanoTime) { - return new File(mFilesDir, getUniqueFilename(LOG_FILENAME_PREFIX, time, nanoTime)); - } - - public File getUserRecordingFilePath(final long time, final long nanoTime) { - return new File(mFilesDir, getUniqueFilename(USER_RECORDING_FILENAME_PREFIX, time, - nanoTime)); - } - - private static String getUniqueFilename(final String prefix, final long time, - final long nanoTime) { - return prefix + "-" + time + "-" + nanoTime + FILENAME_SUFFIX; - } -} diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java deleted file mode 100644 index da9c61103..000000000 --- a/java/src/com/android/inputmethod/research/ResearchLogger.java +++ /dev/null @@ -1,1950 +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.research; - -import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.Resources; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Paint.Style; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.os.SystemClock; -import android.preference.PreferenceManager; -import android.text.TextUtils; -import android.util.Log; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.inputmethod.CompletionInfo; -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.KeyboardSwitcher; -import com.android.inputmethod.keyboard.KeyboardView; -import com.android.inputmethod.keyboard.MainKeyboardView; -import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.Dictionary; -import com.android.inputmethod.latin.LatinIME; -import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.RichInputConnection; -import com.android.inputmethod.latin.Suggest; -import com.android.inputmethod.latin.SuggestedWords; -import com.android.inputmethod.latin.define.ProductionFlag; -import com.android.inputmethod.latin.utils.InputTypeUtils; -import com.android.inputmethod.latin.utils.TextRange; -import com.android.inputmethod.research.MotionEventReader.ReplayData; -import com.android.inputmethod.research.ui.SplashScreen; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.List; -import java.util.Random; -import java.util.concurrent.TimeUnit; -import java.util.regex.Pattern; - -// TODO: Add a unit test for every "logging" method (i.e. that is called from the IME and calls -// enqueueEvent to record a LogStatement). -/** - * 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#USES_DEVELOPMENT_ONLY_DIAGNOSTICS}. - */ -public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener, - SplashScreen.UserConsentListener { - // TODO: This class has grown quite large and combines several concerns that should be - // separated. The following refactorings will be applied as soon as possible after adding - // support for replaying historical events, fixing some replay bugs, adding some ui constraints - // on the feedback dialog, and adding the survey dialog. - // TODO: Refactor. Move feedback screen code into separate class. - // TODO: Refactor. Move logging invocations into their own class. - // TODO: Refactor. Move currentLogUnit management into separate class. - private static final String TAG = ResearchLogger.class.getSimpleName(); - private static final boolean DEBUG = false - && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; - private static final boolean DEBUG_REPLAY_AFTER_FEEDBACK = false - && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; - // Whether the TextView contents are logged at the end of the session. true will disclose - // private info. - private static final boolean LOG_FULL_TEXTVIEW_CONTENTS = false - && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; - // Whether the feedback dialog preserves the editable text across invocations. Should be false - // for normal research builds so users do not have to delete the same feedback string they - // entered earlier. Should be true for builds internal to a development team so when the text - // field holds a channel name, the developer does not have to re-enter it when using the - // feedback mechanism to generate multiple tests. - private static final boolean FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD = false; - /* package */ static boolean sIsLogging = false; - private static final int OUTPUT_FORMAT_VERSION = 5; - // Whether all words should be recorded, leaving unsampled word between bigrams. Useful for - // testing. - /* package for test */ static final boolean IS_LOGGING_EVERYTHING = false - && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; - // The number of words between n-grams to omit from the log. - private static final int NUMBER_OF_WORDS_BETWEEN_SAMPLES = - IS_LOGGING_EVERYTHING ? 0 : (DEBUG ? 2 : 18); - - // Whether to show an indicator on the screen that logging is on. Currently a very small red - // dot in the lower right hand corner. Most users should not notice it. - private static final boolean IS_SHOWING_INDICATOR = true; - // Change the default indicator to something very visible. Currently two red vertical bars on - // either side of they keyboard. - private static final boolean IS_SHOWING_INDICATOR_CLEARLY = false || - (IS_LOGGING_EVERYTHING && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG); - // FEEDBACK_WORD_BUFFER_SIZE should add 1 because it must also hold the feedback LogUnit itself. - public static final int FEEDBACK_WORD_BUFFER_SIZE = (Integer.MAX_VALUE - 1) + 1; - - // The special output text to invoke a research feedback dialog. - public static final String RESEARCH_KEY_OUTPUT_TEXT = ".research."; - - // constants related to specific log points - private static final String WHITESPACE_SEPARATORS = " \t\n\r"; - private static final int MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1 - private static final String PREF_RESEARCH_SAVED_CHANNEL = "pref_research_saved_channel"; - - private static final long RESEARCHLOG_CLOSE_TIMEOUT_IN_MS = TimeUnit.SECONDS.toMillis(5); - private static final long RESEARCHLOG_ABORT_TIMEOUT_IN_MS = TimeUnit.SECONDS.toMillis(5); - private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = TimeUnit.DAYS.toMillis(1); - private static final long MAX_LOGFILE_AGE_IN_MS = TimeUnit.DAYS.toMillis(4); - - private static final ResearchLogger sInstance = new ResearchLogger(); - private static String sAccountType = null; - private static String sAllowedAccountDomain = null; - private ResearchLog mMainResearchLog; // always non-null after init() is called - // 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 for test */ MainLogBuffer mMainLogBuffer; // always non-null after init() is called - /* package */ ResearchLog mUserRecordingLog; - /* package */ LogBuffer mUserRecordingLogBuffer; - private File mUserRecordingFile = null; - - private boolean mIsPasswordView = 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"; - 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 MainKeyboardView mMainKeyboardView; - // TODO: Check whether a superclass can be used instead of LatinIME. - /* package for test */ LatinIME mLatinIME; - private final Statistics mStatistics; - private final MotionEventReader mMotionEventReader = new MotionEventReader(); - private final Replayer mReplayer = Replayer.getInstance(); - private ResearchLogDirectory mResearchLogDirectory; - private SplashScreen mSplashScreen; - - private Intent mUploadNowIntent; - - /* package for test */ LogUnit mCurrentLogUnit = new LogUnit(); - - // Gestured or tapped words may be committed after the gesture of the next word has started. - // To ensure that the gesture data of the next word is not associated with the previous word, - // thereby leaking private data, we store the time of the down event that started the second - // gesture, and when committing the earlier word, split the LogUnit. - private long mSavedDownEventTime; - private Bundle mFeedbackDialogBundle = null; - // Whether the feedback dialog is visible, and the user is typing into it. Normal logging is - // not performed on text that the user types into the feedback dialog. - private boolean mInFeedbackDialog = false; - private Handler mUserRecordingTimeoutHandler; - private static final long USER_RECORDING_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(30); - - // Stores a temporary LogUnit while generating a phantom space. Needed because phantom spaces - // are issued out-of-order, immediately before the characters generated by other operations that - // have already outputted LogStatements. - private LogUnit mPhantomSpaceLogUnit = null; - - private ResearchLogger() { - mStatistics = Statistics.getInstance(); - } - - public static ResearchLogger getInstance() { - return sInstance; - } - - public void init(final LatinIME latinIME, final KeyboardSwitcher keyboardSwitcher, - final Suggest suggest) { - assert latinIME != null; - mLatinIME = latinIME; - mPrefs = PreferenceManager.getDefaultSharedPreferences(latinIME); - mPrefs.registerOnSharedPreferenceChangeListener(this); - - // Initialize fields from preferences - sIsLogging = ResearchSettings.readResearchLoggerEnabledFlag(mPrefs); - - // Initialize fields from resources - final Resources res = latinIME.getResources(); - sAccountType = res.getString(R.string.research_account_type); - sAllowedAccountDomain = res.getString(R.string.research_allowed_account_domain); - - // Initialize directory manager - mResearchLogDirectory = new ResearchLogDirectory(mLatinIME); - cleanLogDirectoryIfNeeded(mResearchLogDirectory, System.currentTimeMillis()); - - // Initialize log buffers - resetLogBuffers(); - - // Initialize external services - mUploadNowIntent = new Intent(mLatinIME, UploaderService.class); - mUploadNowIntent.putExtra(UploaderService.EXTRA_UPLOAD_UNCONDITIONALLY, true); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - UploaderService.cancelAndRescheduleUploadingService(mLatinIME, - true /* needsRescheduling */); - } - mReplayer.setKeyboardSwitcher(keyboardSwitcher); - } - - private void resetLogBuffers() { - mMainResearchLog = new ResearchLog(mResearchLogDirectory.getLogFilePath( - System.currentTimeMillis(), System.nanoTime()), mLatinIME); - final int numWordsToIgnore = new Random().nextInt(NUMBER_OF_WORDS_BETWEEN_SAMPLES + 1); - mMainLogBuffer = new MainLogBuffer(NUMBER_OF_WORDS_BETWEEN_SAMPLES, numWordsToIgnore, - mSuggest) { - @Override - protected void publish(final ArrayList<LogUnit> logUnits, - boolean canIncludePrivateData) { - canIncludePrivateData |= IS_LOGGING_EVERYTHING; - for (final LogUnit logUnit : logUnits) { - if (DEBUG) { - final String wordsString = logUnit.getWordsAsString(); - Log.d(TAG, "onPublish: '" + wordsString - + "', hc: " + logUnit.containsUserDeletions() - + ", cipd: " + canIncludePrivateData); - } - for (final String word : logUnit.getWordsAsStringArray()) { - final Dictionary dictionary = getDictionary(); - mStatistics.recordWordEntered( - dictionary != null && dictionary.isValidWord(word), - logUnit.containsUserDeletions()); - } - } - publishLogUnits(logUnits, mMainResearchLog, canIncludePrivateData); - } - }; - } - - private void cleanLogDirectoryIfNeeded(final ResearchLogDirectory researchLogDirectory, - final long now) { - final long lastCleanupTime = ResearchSettings.readResearchLastDirCleanupTime(mPrefs); - if (now - lastCleanupTime < DURATION_BETWEEN_DIR_CLEANUP_IN_MS) return; - final long oldestAllowedFileTime = now - MAX_LOGFILE_AGE_IN_MS; - mResearchLogDirectory.cleanupLogFilesOlderThan(oldestAllowedFileTime); - ResearchSettings.writeResearchLastDirCleanupTime(mPrefs, now); - } - - public void mainKeyboardView_onAttachedToWindow(final MainKeyboardView mainKeyboardView) { - mMainKeyboardView = mainKeyboardView; - maybeShowSplashScreen(); - } - - public void mainKeyboardView_onDetachedFromWindow() { - mMainKeyboardView = null; - } - - public void onDestroy() { - if (mPrefs != null) { - mPrefs.unregisterOnSharedPreferenceChangeListener(this); - } - } - - private void maybeShowSplashScreen() { - if (ResearchSettings.readHasSeenSplash(mPrefs)) return; - if (mSplashScreen != null && mSplashScreen.isShowing()) return; - if (mMainKeyboardView == null) return; - final IBinder windowToken = mMainKeyboardView.getWindowToken(); - if (windowToken == null) return; - - mSplashScreen = new SplashScreen(mLatinIME, this); - mSplashScreen.showSplashScreen(windowToken); - } - - @Override - public void onSplashScreenUserClickedOk() { - if (mPrefs == null) { - mPrefs = PreferenceManager.getDefaultSharedPreferences(mLatinIME); - if (mPrefs == null) return; - } - sIsLogging = true; - ResearchSettings.writeResearchLoggerEnabledFlag(mPrefs, true); - ResearchSettings.writeHasSeenSplash(mPrefs, true); - restart(); - } - - private void checkForEmptyEditor() { - if (mLatinIME == null) { - return; - } - final InputConnection ic = mLatinIME.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(); - requestIndicatorRedraw(); - mStatistics.reset(); - checkForEmptyEditor(); - } - - /* package */ void stop() { - if (DEBUG) { - Log.d(TAG, "stop called"); - } - // Commit mCurrentLogUnit before closing. - commitCurrentLogUnit(); - - try { - mMainLogBuffer.shiftAndPublishAll(); - } catch (final IOException e) { - Log.w(TAG, "IOException when publishing LogBuffer", e); - } - logStatistics(); - commitCurrentLogUnit(); - mMainLogBuffer.setIsStopping(); - try { - mMainLogBuffer.shiftAndPublishAll(); - } catch (final IOException e) { - Log.w(TAG, "IOException when publishing LogBuffer", e); - } - mMainResearchLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS); - - resetLogBuffers(); - cancelFeedbackDialog(); - } - - public void abort() { - if (DEBUG) { - Log.d(TAG, "abort called"); - } - mMainLogBuffer.clear(); - mMainResearchLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS); - - resetLogBuffers(); - } - - private void restart() { - stop(); - start(); - } - - @Override - public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) { - if (key == null || prefs == null) { - return; - } - requestIndicatorRedraw(); - mPrefs = prefs; - prefsChanged(prefs); - } - - public void onResearchKeySelected(final LatinIME latinIME) { - mCurrentLogUnit.removeResearchButtonInvocation(); - if (mInFeedbackDialog) { - Toast.makeText(latinIME, R.string.research_please_exit_feedback_form, - Toast.LENGTH_LONG).show(); - return; - } - presentFeedbackDialog(latinIME); - } - - public void presentFeedbackDialogFromSettings() { - if (mLatinIME != null) { - presentFeedbackDialog(mLatinIME); - } - } - - public void presentFeedbackDialog(final LatinIME latinIME) { - if (isMakingUserRecording()) { - saveRecording(); - } - mInFeedbackDialog = true; - - final Intent intent = new Intent(); - intent.setClass(mLatinIME, FeedbackActivity.class); - if (mFeedbackDialogBundle == null) { - // Restore feedback field with channel name - final Bundle bundle = new Bundle(); - bundle.putBoolean(FeedbackFragment.KEY_INCLUDE_ACCOUNT_NAME, true); - bundle.putBoolean(FeedbackFragment.KEY_HAS_USER_RECORDING, false); - if (FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD) { - final String savedChannelName = mPrefs.getString(PREF_RESEARCH_SAVED_CHANNEL, ""); - bundle.putString(FeedbackFragment.KEY_FEEDBACK_STRING, savedChannelName); - } - mFeedbackDialogBundle = bundle; - } - intent.putExtras(mFeedbackDialogBundle); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - latinIME.startActivity(intent); - } - - public void setFeedbackDialogBundle(final Bundle bundle) { - mFeedbackDialogBundle = bundle; - } - - public void startRecording() { - final Resources res = mLatinIME.getResources(); - Toast.makeText(mLatinIME, - res.getString(R.string.research_feedback_demonstration_instructions), - Toast.LENGTH_LONG).show(); - startRecordingInternal(); - } - - private void startRecordingInternal() { - if (mUserRecordingLog != null) { - mUserRecordingLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS); - } - mUserRecordingFile = mResearchLogDirectory.getUserRecordingFilePath( - System.currentTimeMillis(), System.nanoTime()); - mUserRecordingLog = new ResearchLog(mUserRecordingFile, mLatinIME); - mUserRecordingLogBuffer = new LogBuffer(); - resetRecordingTimer(); - } - - private boolean isMakingUserRecording() { - return mUserRecordingLog != null; - } - - private void resetRecordingTimer() { - if (mUserRecordingTimeoutHandler == null) { - mUserRecordingTimeoutHandler = new Handler(); - } - clearRecordingTimer(); - mUserRecordingTimeoutHandler.postDelayed(mRecordingHandlerTimeoutRunnable, - USER_RECORDING_TIMEOUT_MS); - } - - private void clearRecordingTimer() { - mUserRecordingTimeoutHandler.removeCallbacks(mRecordingHandlerTimeoutRunnable); - } - - private Runnable mRecordingHandlerTimeoutRunnable = new Runnable() { - @Override - public void run() { - cancelRecording(); - requestIndicatorRedraw(); - final Resources res = mLatinIME.getResources(); - Toast.makeText(mLatinIME, res.getString(R.string.research_feedback_recording_failure), - Toast.LENGTH_LONG).show(); - } - }; - - private void cancelRecording() { - if (mUserRecordingLog != null) { - mUserRecordingLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS); - } - mUserRecordingLog = null; - mUserRecordingLogBuffer = null; - if (mFeedbackDialogBundle != null) { - mFeedbackDialogBundle.putBoolean("HasRecording", false); - } - } - - private void saveRecording() { - commitCurrentLogUnit(); - publishLogBuffer(mUserRecordingLogBuffer, mUserRecordingLog, true); - mUserRecordingLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS); - mUserRecordingLog = null; - mUserRecordingLogBuffer = null; - - if (mFeedbackDialogBundle != null) { - mFeedbackDialogBundle.putBoolean(FeedbackFragment.KEY_HAS_USER_RECORDING, true); - } - clearRecordingTimer(); - } - - // 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 - + TimeUnit.MINUTES.toMillis(SUSPEND_DURATION_IN_MINUTES); - suspendLoggingUntil(resumeTime); - toast.cancel(); - Toast.makeText(latinIME, R.string.research_notify_logging_suspended, - Toast.LENGTH_LONG).show(); - } - } - */ - - /** - * Get the name of the first allowed account on the device. - * - * Allowed accounts must be in the domain given by ALLOWED_ACCOUNT_DOMAIN. - * - * @return The user's account name. - */ - public String getAccountName() { - if (sAccountType == null || sAccountType.isEmpty()) { - return null; - } - if (sAllowedAccountDomain == null || sAllowedAccountDomain.isEmpty()) { - return null; - } - final AccountManager manager = AccountManager.get(mLatinIME); - // Filter first by account type. - final Account[] accounts = manager.getAccountsByType(sAccountType); - - for (final Account account : accounts) { - if (DEBUG) { - Log.d(TAG, account.name); - } - final String[] parts = account.name.split("@"); - if (parts.length > 1 && parts[1].equals(sAllowedAccountDomain)) { - return parts[0]; - } - } - return null; - } - - private static final LogStatement LOGSTATEMENT_FEEDBACK = - new LogStatement("UserFeedback", false, false, "contents", "accountName", "recording"); - public void sendFeedback(final String feedbackContents, final boolean includeHistory, - final boolean isIncludingAccountName, final boolean isIncludingRecording) { - String recording = ""; - if (isIncludingRecording) { - // Try to read recording from recently written json file - if (mUserRecordingFile != null) { - FileChannel channel = null; - try { - channel = new FileInputStream(mUserRecordingFile).getChannel(); - final MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, - channel.size()); - // Android's openFileOutput() creates the file, so we use Android's default - // Charset (UTF-8) here to read it. - recording = Charset.defaultCharset().decode(buffer).toString(); - } catch (FileNotFoundException e) { - Log.e(TAG, "Could not find recording file", e); - } catch (IOException e) { - Log.e(TAG, "Error reading recording file", e); - } finally { - if (channel != null) { - try { - channel.close(); - } catch (IOException e) { - Log.e(TAG, "Error closing recording file", e); - } - } - } - } - } - final LogUnit feedbackLogUnit = new LogUnit(); - final String accountName = isIncludingAccountName ? getAccountName() : ""; - feedbackLogUnit.addLogStatement(LOGSTATEMENT_FEEDBACK, SystemClock.uptimeMillis(), - feedbackContents, accountName, recording); - - final ResearchLog feedbackLog = new FeedbackLog(mResearchLogDirectory.getLogFilePath( - System.currentTimeMillis(), System.nanoTime()), mLatinIME); - final LogBuffer feedbackLogBuffer = new LogBuffer(); - feedbackLogBuffer.shiftIn(feedbackLogUnit); - publishLogBuffer(feedbackLogBuffer, feedbackLog, true /* isIncludingPrivateData */); - feedbackLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS); - uploadNow(); - - if (isIncludingRecording && DEBUG_REPLAY_AFTER_FEEDBACK) { - final Handler handler = new Handler(); - handler.postDelayed(new Runnable() { - @Override - public void run() { - final ReplayData replayData = - mMotionEventReader.readMotionEventData(mUserRecordingFile); - mReplayer.replay(replayData, null); - } - }, TimeUnit.SECONDS.toMillis(1)); - } - - if (FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD) { - // Use feedback string as a channel name to label feedback strings. Here we record the - // string for prepopulating the field next time. - final String channelName = feedbackContents; - if (mPrefs == null) { - return; - } - mPrefs.edit().putString(PREF_RESEARCH_SAVED_CHANNEL, channelName).apply(); - } - } - - public void uploadNow() { - if (DEBUG) { - Log.d(TAG, "calling uploadNow()"); - } - mLatinIME.startService(mUploadNowIntent); - } - - public void onLeavingSendFeedbackDialog() { - mInFeedbackDialog = false; - } - - private void cancelFeedbackDialog() { - if (isMakingUserRecording()) { - cancelRecording(); - } - mInFeedbackDialog = false; - } - - public void initSuggest(final Suggest suggest) { - mSuggest = suggest; - // MainLogBuffer now has an out-of-date Suggest object. Close down MainLogBuffer and create - // a new one. - if (mMainLogBuffer != null) { - restart(); - } - } - - private Dictionary getDictionary() { - if (mSuggest == null) { - return null; - } - return mSuggest.getMainDictionary(); - } - - private void setIsPasswordView(boolean isPasswordView) { - mIsPasswordView = isPasswordView; - } - - /** - * Returns true if logging is permitted. - * - * This method is called when adding a LogStatement to a LogUnit, and when adding a LogUnit to a - * ResearchLog. It is checked in both places in case conditions change between these times, and - * as a defensive measure in case refactoring changes the logging pipeline. - */ - private boolean isAllowedToLogTo(final ResearchLog researchLog) { - // Logging is never allowed in these circumstances - if (mIsPasswordView) return false; - if (!sIsLogging) return false; - if (mInFeedbackDialog) { - // The FeedbackDialog is up. Normal logging should not happen (the user might be trying - // out things while the dialog is up, and their reporting of an issue may not be - // representative of what they normally type). However, after the user has finished - // entering their feedback, the logger packs their comments and an encoded version of - // any demonstration of the issue into a special "FeedbackLog". So if the FeedbackLog - // is the destination, we do want to allow logging to it. - return researchLog.isFeedbackLog(); - } - // No other exclusions. Logging is permitted. - return true; - } - - public void requestIndicatorRedraw() { - if (!IS_SHOWING_INDICATOR) { - return; - } - if (mMainKeyboardView == null) { - return; - } - mMainKeyboardView.invalidateAllKeys(); - } - - private boolean isReplaying() { - return mReplayer.isReplaying(); - } - - private int getIndicatorColor() { - if (isMakingUserRecording()) { - return Color.YELLOW; - } - if (isReplaying()) { - return Color.GREEN; - } - return Color.RED; - } - - 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 the indicator only decorates the main - // keyboard, not every keyboard. - if (IS_SHOWING_INDICATOR && (isAllowedToLogTo(mMainResearchLog) || isReplaying()) - && view instanceof MainKeyboardView) { - final int savedColor = paint.getColor(); - paint.setColor(getIndicatorColor()); - final Style savedStyle = paint.getStyle(); - paint.setStyle(Style.STROKE); - final float savedStrokeWidth = paint.getStrokeWidth(); - if (IS_SHOWING_INDICATOR_CLEARLY) { - paint.setStrokeWidth(5); - canvas.drawLine(0, 0, 0, height, paint); - canvas.drawLine(width, 0, width, height, paint); - } else { - // Put a tiny 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 - 1, height - 1, width, height, paint); - } - paint.setColor(savedColor); - paint.setStyle(savedStyle); - paint.setStrokeWidth(savedStrokeWidth); - } - } - - /** - * Buffer a research log event, flagging it as privacy-sensitive. - */ - private synchronized void enqueueEvent(final LogStatement logStatement, - final Object... values) { - enqueueEvent(mCurrentLogUnit, logStatement, values); - } - - private synchronized void enqueueEvent(final LogUnit logUnit, final LogStatement logStatement, - final Object... values) { - assert values.length == logStatement.getKeys().length; - if (isAllowedToLogTo(mMainResearchLog) && logUnit != null) { - final long time = SystemClock.uptimeMillis(); - logUnit.addLogStatement(logStatement, time, values); - } - } - - private void setCurrentLogUnitContainsDigitFlag() { - mCurrentLogUnit.setMayContainDigit(); - } - - private void setCurrentLogUnitContainsUserDeletions() { - mCurrentLogUnit.setContainsUserDeletions(); - } - - private void setCurrentLogUnitCorrectionType(final int correctionType) { - mCurrentLogUnit.setCorrectionType(correctionType); - } - - /* package for test */ void commitCurrentLogUnit() { - if (DEBUG) { - Log.d(TAG, "commitCurrentLogUnit" + (mCurrentLogUnit.hasOneOrMoreWords() ? - ": " + mCurrentLogUnit.getWordsAsString() : "")); - } - if (!mCurrentLogUnit.isEmpty()) { - mMainLogBuffer.shiftIn(mCurrentLogUnit); - if (mUserRecordingLogBuffer != null) { - mUserRecordingLogBuffer.shiftIn(mCurrentLogUnit); - } - mCurrentLogUnit = new LogUnit(); - } else { - if (DEBUG) { - Log.d(TAG, "Warning: tried to commit empty log unit."); - } - } - } - - private static final LogStatement LOGSTATEMENT_UNCOMMIT_CURRENT_LOGUNIT = - new LogStatement("UncommitCurrentLogUnit", false, false); - public void uncommitCurrentLogUnit(final String expectedWord, - final boolean dumpCurrentLogUnit) { - // The user has deleted this word and returned to the previous. Check that the word in the - // logUnit matches the expected word. If so, restore the last log unit committed to be the - // current logUnit. I.e., pull out the last LogUnit from all the LogBuffers, and make - // it the mCurrentLogUnit so the new edits are captured with the word. Optionally dump the - // contents of mCurrentLogUnit (useful if they contain deletions of the next word that - // should not be reported to protect user privacy) - // - // Note that we don't use mLastLogUnit here, because it only goes one word back and is only - // needed for reverts, which only happen one back. - final LogUnit oldLogUnit = mMainLogBuffer.peekLastLogUnit(); - - // Check that expected word matches. It's ok if both strings are null, because this is the - // case where the LogUnit is storing a non-word, e.g. a separator. - if (oldLogUnit != null) { - // Because the word is stored in the LogUnit with digits scrubbed, the comparison must - // be made on a scrubbed version of the expectedWord as well. - final String scrubbedExpectedWord = scrubDigitsFromString(expectedWord); - final String oldLogUnitWords = oldLogUnit.getWordsAsString(); - if (!TextUtils.equals(scrubbedExpectedWord, oldLogUnitWords)) return; - } - - // Uncommit, merging if necessary. - mMainLogBuffer.unshiftIn(); - if (oldLogUnit != null && !dumpCurrentLogUnit) { - oldLogUnit.append(mCurrentLogUnit); - mSavedDownEventTime = Long.MAX_VALUE; - } - if (oldLogUnit == null) { - mCurrentLogUnit = new LogUnit(); - } else { - mCurrentLogUnit = oldLogUnit; - } - enqueueEvent(LOGSTATEMENT_UNCOMMIT_CURRENT_LOGUNIT); - if (DEBUG) { - Log.d(TAG, "uncommitCurrentLogUnit (dump=" + dumpCurrentLogUnit + ") back to " - + (mCurrentLogUnit.hasOneOrMoreWords() ? ": '" - + mCurrentLogUnit.getWordsAsString() + "'" : "")); - } - } - - /** - * Publish all the logUnits in the logBuffer, without doing any privacy filtering. - */ - /* package for test */ void publishLogBuffer(final LogBuffer logBuffer, - final ResearchLog researchLog, final boolean canIncludePrivateData) { - publishLogUnits(logBuffer.getLogUnits(), researchLog, canIncludePrivateData); - } - - private static final LogStatement LOGSTATEMENT_LOG_SEGMENT_OPENING = - new LogStatement("logSegmentStart", false, false, "isIncludingPrivateData"); - private static final LogStatement LOGSTATEMENT_LOG_SEGMENT_CLOSING = - new LogStatement("logSegmentEnd", false, false); - /** - * Publish all LogUnits in a list. - * - * Any privacy checks should be performed before calling this method. - */ - /* package for test */ void publishLogUnits(final List<LogUnit> logUnits, - final ResearchLog researchLog, final boolean canIncludePrivateData) { - final LogUnit openingLogUnit = new LogUnit(); - if (logUnits.isEmpty()) return; - if (!isAllowedToLogTo(researchLog)) return; - // LogUnits not containing private data, such as contextual data for the log, do not require - // logSegment boundary statements. - if (canIncludePrivateData) { - openingLogUnit.addLogStatement(LOGSTATEMENT_LOG_SEGMENT_OPENING, - SystemClock.uptimeMillis(), canIncludePrivateData); - researchLog.publish(openingLogUnit, true /* isIncludingPrivateData */); - } - for (LogUnit logUnit : logUnits) { - if (DEBUG) { - Log.d(TAG, "publishLogBuffer: " + (logUnit.hasOneOrMoreWords() - ? logUnit.getWordsAsString() : "<wordless>") - + ", correction?: " + logUnit.containsUserDeletions()); - } - researchLog.publish(logUnit, canIncludePrivateData); - } - if (canIncludePrivateData) { - final LogUnit closingLogUnit = new LogUnit(); - closingLogUnit.addLogStatement(LOGSTATEMENT_LOG_SEGMENT_CLOSING, - SystemClock.uptimeMillis()); - researchLog.publish(closingLogUnit, true /* isIncludingPrivateData */); - } - } - - public static boolean hasLetters(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 true; - } - } - return false; - } - - /** - * Commit the portion of mCurrentLogUnit before maxTime as a worded logUnit. - * - * After this operation completes, mCurrentLogUnit will hold any logStatements that happened - * after maxTime. - */ - /* package for test */ void commitCurrentLogUnitAsWord(final String word, final long maxTime, - final boolean isBatchMode) { - if (word == null) { - return; - } - if (word.length() > 0 && hasLetters(word)) { - mCurrentLogUnit.setWords(word); - } - final LogUnit newLogUnit = mCurrentLogUnit.splitByTime(maxTime); - enqueueCommitText(word, isBatchMode); - commitCurrentLogUnit(); - mCurrentLogUnit = newLogUnit; - } - - /** - * Record the time of a MotionEvent.ACTION_DOWN. - * - * Warning: Not thread safe. Only call from the main thread. - */ - private void setSavedDownEventTime(final long time) { - mSavedDownEventTime = time; - } - - public void onWordFinished(final String word, final boolean isBatchMode) { - commitCurrentLogUnitAsWord(word, mSavedDownEventTime, isBatchMode); - mSavedDownEventTime = Long.MAX_VALUE; - } - - private static int scrubDigitFromCodePoint(int codePoint) { - return Character.isDigit(codePoint) ? DIGIT_REPLACEMENT_CODEPOINT : codePoint; - } - - /* package for test */ static String scrubDigitsFromString(final String s) { - if (s == null) return null; - 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 String scrubWord(String word) { - final Dictionary dictionary = getDictionary(); - if (dictionary == null) { - return WORD_REPLACEMENT_STRING; - } - if (dictionary.isValidWord(word)) { - return word; - } - return WORD_REPLACEMENT_STRING; - } - - // Specific logging methods follow below. The comments for each logging method should - // indicate what specific method is logged, and how to trigger it from the user interface. - // - // Logging methods can be generally classified into two flavors, "UserAction", which should - // correspond closely to an event that is sensed by the IME, and is usually generated - // directly by the user, and "SystemResponse" which corresponds to an event that the IME - // generates, often after much processing of user input. SystemResponses should correspond - // closely to user-visible events. - // TODO: Consider exposing the UserAction classification in the log output. - - /** - * Log a call to LatinIME.onStartInputViewInternal(). - * - * UserAction: called each time the keyboard is opened up. - */ - private static final LogStatement LOGSTATEMENT_LATIN_IME_ON_START_INPUT_VIEW_INTERNAL = - new LogStatement("LatinImeOnStartInputViewInternal", false, false, "uuid", - "packageName", "inputType", "imeOptions", "fieldId", "display", "model", - "prefs", "versionCode", "versionName", "outputFormatVersion", "logEverything", - "isDevTeamBuild"); - public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo, - final SharedPreferences prefs) { - final ResearchLogger researchLogger = getInstance(); - if (editorInfo != null) { - final boolean isPassword = InputTypeUtils.isPasswordInputType(editorInfo.inputType) - || InputTypeUtils.isVisiblePasswordInputType(editorInfo.inputType); - getInstance().setIsPasswordView(isPassword); - researchLogger.start(); - final Context context = researchLogger.mLatinIME; - try { - final PackageInfo packageInfo; - packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), - 0); - final Integer versionCode = packageInfo.versionCode; - final String versionName = packageInfo.versionName; - final String uuid = ResearchSettings.readResearchLoggerUuid(researchLogger.mPrefs); - researchLogger.enqueueEvent(LOGSTATEMENT_LATIN_IME_ON_START_INPUT_VIEW_INTERNAL, - uuid, editorInfo.packageName, Integer.toHexString(editorInfo.inputType), - Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId, - Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName, - OUTPUT_FORMAT_VERSION, IS_LOGGING_EVERYTHING, - researchLogger.isDevTeamBuild()); - // Commit the logUnit so the LatinImeOnStartInputViewInternal event is in its own - // logUnit at the beginning of the log. - researchLogger.commitCurrentLogUnit(); - } catch (final NameNotFoundException e) { - Log.e(TAG, "NameNotFound", e); - } - } - } - - // TODO: Update this heuristic pattern to something more reliable. Developer builds tend to - // have the developer name and year embedded. - private static final Pattern developerBuildRegex = Pattern.compile("[A-Za-z]\\.20[1-9]"); - private boolean isDevTeamBuild() { - try { - final PackageInfo packageInfo; - packageInfo = mLatinIME.getPackageManager().getPackageInfo(mLatinIME.getPackageName(), - 0); - final String versionName = packageInfo.versionName; - return developerBuildRegex.matcher(versionName).find(); - } catch (final NameNotFoundException e) { - Log.e(TAG, "Could not determine package name", e); - return false; - } - } - - /** - * Log a change in preferences. - * - * UserAction: called when the user changes the settings. - */ - private static final LogStatement LOGSTATEMENT_PREFS_CHANGED = - new LogStatement("PrefsChanged", false, false, "prefs"); - public static void prefsChanged(final SharedPreferences prefs) { - final ResearchLogger researchLogger = getInstance(); - researchLogger.enqueueEvent(LOGSTATEMENT_PREFS_CHANGED, prefs); - } - - /** - * Log a call to MainKeyboardView.processMotionEvent(). - * - * UserAction: called when the user puts their finger onto the screen (ACTION_DOWN). - * - */ - private static final LogStatement LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT = - new LogStatement("MotionEvent", true, false, "action", - LogStatement.KEY_IS_LOGGING_RELATED, "motionEvent"); - public static void mainKeyboardView_processMotionEvent(final MotionEvent me) { - if (me == null) { - return; - } - final int action = me.getActionMasked(); - final long eventTime = me.getEventTime(); - final String actionString = LoggingUtils.getMotionEventActionTypeString(action); - final ResearchLogger researchLogger = getInstance(); - researchLogger.enqueueEvent(LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT, - actionString, false /* IS_LOGGING_RELATED */, MotionEvent.obtain(me)); - if (action == MotionEvent.ACTION_DOWN) { - // Subtract 1 from eventTime so the down event is included in the later - // LogUnit, not the earlier (the test is for inequality). - researchLogger.setSavedDownEventTime(eventTime - 1); - } - // Refresh the timer in case we are capturing user feedback. - if (researchLogger.isMakingUserRecording()) { - researchLogger.resetRecordingTimer(); - } - } - - /** - * Log a call to LatinIME.onCodeInput(). - * - * SystemResponse: The main processing step for entering text. Called when the user performs a - * tap, a flick, a long press, releases a gesture, or taps a punctuation suggestion. - */ - private static final LogStatement LOGSTATEMENT_LATIN_IME_ON_CODE_INPUT = - new LogStatement("LatinImeOnCodeInput", true, false, "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(); - researchLogger.enqueueEvent(LOGSTATEMENT_LATIN_IME_ON_CODE_INPUT, - Constants.printableCode(scrubDigitFromCodePoint(code)), x, y); - if (Character.isDigit(code)) { - researchLogger.setCurrentLogUnitContainsDigitFlag(); - } - researchLogger.mStatistics.recordChar(code, time); - } - /** - * Log a call to LatinIME.onDisplayCompletions(). - * - * SystemResponse: The IME has displayed application-specific completions. They may show up - * in the suggestion strip, such as a landscape phone. - */ - private static final LogStatement LOGSTATEMENT_LATINIME_ONDISPLAYCOMPLETIONS = - new LogStatement("LatinIMEOnDisplayCompletions", true, true, - "applicationSpecifiedCompletions"); - public static void latinIME_onDisplayCompletions( - final CompletionInfo[] applicationSpecifiedCompletions) { - // Note; passing an array as a single element in a vararg list. Must create a new - // dummy array around it or it will get expanded. - getInstance().enqueueEvent(LOGSTATEMENT_LATINIME_ONDISPLAYCOMPLETIONS, - new Object[] { applicationSpecifiedCompletions }); - } - - public static boolean getAndClearLatinIMEExpectingUpdateSelection() { - boolean returnValue = sLatinIMEExpectingUpdateSelection; - sLatinIMEExpectingUpdateSelection = false; - return returnValue; - } - - /** - * The IME is finishing; it is either being destroyed, or is about to be hidden. - * - * UserAction: The user has performed an action that has caused the IME to be closed. They may - * have focused on something other than a text field, or explicitly closed it. - */ - private static final LogStatement LOGSTATEMENT_LATINIME_ONFINISHINPUTVIEWINTERNAL = - new LogStatement("LatinIMEOnFinishInputViewInternal", false, false, "isTextTruncated", - "text"); - public static void latinIME_onFinishInputViewInternal(final boolean finishingInput, - final int savedSelectionStart, final int savedSelectionEnd, final InputConnection ic) { - // The finishingInput flag is set in InputMethodService. It is true if called from - // doFinishInput(), which can be called as part of doStartInput(). This can happen at times - // when the IME is not closing, such as when powering up. The finishinInput flag is false - // if called from finishViews(), which is called from hideWindow() and onDestroy(). These - // are the situations in which we want to finish up the researchLog. - if (ic != null && !finishingInput) { - final boolean isTextTruncated; - final String text; - if (LOG_FULL_TEXTVIEW_CONTENTS) { - // Capture the TextView contents. This will trigger onUpdateSelection(), so we - // set sLatinIMEExpectingUpdateSelection so that when onUpdateSelection() is called, - // it can tell that it was generated by the logging code, and not by the user, and - // therefore keep user-visible state as is. - ic.beginBatchEdit(); - ic.performContextMenuAction(android.R.id.selectAll); - CharSequence charSequence = ic.getSelectedText(0); - if (savedSelectionStart != -1 && savedSelectionEnd != -1) { - ic.setSelection(savedSelectionStart, savedSelectionEnd); - } - ic.endBatchEdit(); - sLatinIMEExpectingUpdateSelection = true; - if (TextUtils.isEmpty(charSequence)) { - isTextTruncated = false; - text = ""; - } else { - if (charSequence.length() > MAX_INPUTVIEW_LENGTH_TO_CAPTURE) { - int length = MAX_INPUTVIEW_LENGTH_TO_CAPTURE; - // do not cut in the middle of a supplementary character - final char c = charSequence.charAt(length - 1); - if (Character.isHighSurrogate(c)) { - length--; - } - final CharSequence truncatedCharSequence = charSequence.subSequence(0, - length); - isTextTruncated = true; - text = truncatedCharSequence.toString(); - } else { - isTextTruncated = false; - text = charSequence.toString(); - } - } - } else { - isTextTruncated = true; - text = ""; - } - final ResearchLogger researchLogger = getInstance(); - // Assume that OUTPUT_ENTIRE_BUFFER is only true when we don't care about privacy (e.g. - // during a live user test), so the normal isPotentiallyPrivate and - // isPotentiallyRevealing flags do not apply - researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONFINISHINPUTVIEWINTERNAL, - isTextTruncated, text); - researchLogger.commitCurrentLogUnit(); - getInstance().stop(); - } - } - - /** - * Log a call to LatinIME.onUpdateSelection(). - * - * UserAction/SystemResponse: The user has moved the cursor or selection. This function may - * be called, however, when the system has moved the cursor, say by inserting a character. - */ - private static final LogStatement LOGSTATEMENT_LATINIME_ONUPDATESELECTION = - new LogStatement("LatinIMEOnUpdateSelection", true, false, "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) { - TextRange range = connection.getWordRangeAtCursor(WHITESPACE_SEPARATORS, 1); - if (range != null) { - word = range.mWord.toString(); - } - } - final ResearchLogger researchLogger = getInstance(); - final String scrubbedWord = researchLogger.scrubWord(word); - researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONUPDATESELECTION, lastSelectionStart, - lastSelectionEnd, oldSelStart, oldSelEnd, newSelStart, newSelEnd, - composingSpanStart, composingSpanEnd, expectingUpdateSelection, - expectingUpdateSelectionFromLogger, scrubbedWord); - } - - /** - * Log a call to LatinIME.onTextInput(). - * - * SystemResponse: Raw text is added to the TextView. - */ - public static void latinIME_onTextInput(final String text, final boolean isBatchMode) { - final ResearchLogger researchLogger = getInstance(); - researchLogger.commitCurrentLogUnitAsWord(text, Long.MAX_VALUE, isBatchMode); - } - - /** - * Log a revert of onTextInput() (known in the IME as "EnteredText"). - * - * SystemResponse: Remove the LogUnit recording the textInput - */ - public static void latinIME_handleBackspace_cancelTextInput(final String text) { - final ResearchLogger researchLogger = getInstance(); - researchLogger.uncommitCurrentLogUnit(text, true /* dumpCurrentLogUnit */); - } - - /** - * Log a call to LatinIME.pickSuggestionManually(). - * - * UserAction: The user has chosen a specific word from the suggestion strip. - */ - private static final LogStatement LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY = - new LogStatement("LatinIMEPickSuggestionManually", true, false, "replacedWord", "index", - "suggestion", "x", "y", "isBatchMode", "score", "kind", "sourceDict"); - /** - * Log a call to LatinIME.pickSuggestionManually(). - * - * @param replacedWord the typed word that this manual suggestion replaces. May not be null. - * @param index the index in the suggestion strip - * @param suggestion the committed suggestion. May not be null. - * @param isBatchMode whether this was input in batch mode, aka gesture. - * @param score the internal score of the suggestion, as output by the dictionary - * @param kind the kind of suggestion, as one of the SuggestedWordInfo#KIND_* constants - * @param sourceDict the source origin of this word, as one of the Dictionary#TYPE_* constants. - */ - public static void latinIME_pickSuggestionManually(final String replacedWord, - final int index, final String suggestion, final boolean isBatchMode, - final int score, final int kind, final String sourceDict) { - final ResearchLogger researchLogger = getInstance(); - // Note : suggestion can't be null here, because it's only called in a place where it - // can't be null. - if (!replacedWord.equals(suggestion.toString())) { - // The user chose something other than what was already there. - researchLogger.setCurrentLogUnitContainsUserDeletions(); - researchLogger.setCurrentLogUnitCorrectionType(LogUnit.CORRECTIONTYPE_TYPO); - } - final String scrubbedWord = scrubDigitsFromString(suggestion); - researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY, - scrubDigitsFromString(replacedWord), index, - scrubbedWord, Constants.SUGGESTION_STRIP_COORDINATE, - Constants.SUGGESTION_STRIP_COORDINATE, isBatchMode, score, kind, sourceDict); - researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, Long.MAX_VALUE, isBatchMode); - researchLogger.mStatistics.recordManualSuggestion(SystemClock.uptimeMillis()); - } - - /** - * Log a call to LatinIME.punctuationSuggestion(). - * - * UserAction: The user has chosen punctuation from the punctuation suggestion strip. - */ - private static final LogStatement LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION = - new LogStatement("LatinIMEPunctuationSuggestion", false, false, "index", "suggestion", - "x", "y", "isPrediction"); - public static void latinIME_punctuationSuggestion(final int index, final String suggestion, - final boolean isBatchMode, final boolean isPrediction) { - final ResearchLogger researchLogger = getInstance(); - researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION, index, suggestion, - Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE, - isPrediction); - researchLogger.commitCurrentLogUnitAsWord(suggestion, Long.MAX_VALUE, isBatchMode); - } - - /** - * Log a call to LatinIME.sendKeyCodePoint(). - * - * SystemResponse: The IME is inserting text into the TextView for non-word-constituent, - * strings (separators, numbers, other symbols). - */ - private static final LogStatement LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT = - new LogStatement("LatinIMESendKeyCodePoint", true, false, "code"); - public static void latinIME_sendKeyCodePoint(final int code) { - final ResearchLogger researchLogger = getInstance(); - final LogUnit phantomSpaceLogUnit = researchLogger.mPhantomSpaceLogUnit; - if (phantomSpaceLogUnit == null) { - researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT, - Constants.printableCode(scrubDigitFromCodePoint(code))); - if (Character.isDigit(code)) { - researchLogger.setCurrentLogUnitContainsDigitFlag(); - } - researchLogger.commitCurrentLogUnit(); - } else { - researchLogger.enqueueEvent(phantomSpaceLogUnit, LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT, - Constants.printableCode(scrubDigitFromCodePoint(code))); - if (Character.isDigit(code)) { - phantomSpaceLogUnit.setMayContainDigit(); - } - researchLogger.mMainLogBuffer.shiftIn(phantomSpaceLogUnit); - if (researchLogger.mUserRecordingLogBuffer != null) { - researchLogger.mUserRecordingLogBuffer.shiftIn(phantomSpaceLogUnit); - } - researchLogger.mPhantomSpaceLogUnit = null; - } - } - - /** - * Log a call to LatinIME.promotePhantomSpace(). - * - * SystemResponse: The IME is inserting a real space in place of a phantom space. - */ - private static final LogStatement LOGSTATEMENT_LATINIME_PROMOTEPHANTOMSPACE = - new LogStatement("LatinIMEPromotePhantomSpace", false, false); - public static void latinIME_promotePhantomSpace() { - // A phantom space is always added before the text that triggered it. The triggering text - // and the events that created it will be in mCurrentLogUnit, but the phantom space should - // be in its own LogUnit, committed before the triggering text. Although it is created - // here, it is not added to the LogBuffer until the following call to - // latinIME_sendKeyCodePoint, because SENDKEYCODEPOINT LogStatement also must go into that - // LogUnit. - final ResearchLogger researchLogger = getInstance(); - researchLogger.mPhantomSpaceLogUnit = new LogUnit(); - researchLogger.enqueueEvent(researchLogger.mPhantomSpaceLogUnit, - LOGSTATEMENT_LATINIME_PROMOTEPHANTOMSPACE); - } - - /** - * Log a call to LatinIME.swapSwapperAndSpace(). - * - * SystemResponse: A symbol has been swapped with a space character. E.g. punctuation may swap - * if a soft space is inserted after a word. - */ - private static final LogStatement LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE = - new LogStatement("LatinIMESwapSwapperAndSpace", false, false, "originalCharacters", - "charactersAfterSwap"); - public static void latinIME_swapSwapperAndSpace(final CharSequence originalCharacters, - final String charactersAfterSwap) { - final ResearchLogger researchLogger = getInstance(); - final LogUnit logUnit; - logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit(); - if (logUnit != null) { - researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE, - originalCharacters, charactersAfterSwap); - } - } - - /** - * Log a call to LatinIME.maybeDoubleSpacePeriod(). - * - * SystemResponse: Two spaces have been replaced by period space. - */ - public static void latinIME_maybeDoubleSpacePeriod(final String text, - final boolean isBatchMode) { - final ResearchLogger researchLogger = getInstance(); - researchLogger.commitCurrentLogUnitAsWord(text, Long.MAX_VALUE, isBatchMode); - } - - /** - * Log a call to MainKeyboardView.onLongPress(). - * - * UserAction: The user has performed a long-press on a key. - */ - private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_ONLONGPRESS = - new LogStatement("MainKeyboardViewOnLongPress", false, false); - public static void mainKeyboardView_onLongPress() { - getInstance().enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_ONLONGPRESS); - } - - /** - * Log a call to MainKeyboardView.setKeyboard(). - * - * SystemResponse: The IME has switched to a new keyboard (e.g. French, English). - * This is typically called right after LatinIME.onStartInputViewInternal (when starting a new - * IME), but may happen at other times if the user explicitly requests a keyboard change. - */ - private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD = - new LogStatement("MainKeyboardViewSetKeyboard", false, false, "elementId", "locale", - "orientation", "width", "modeName", "action", "navigateNext", - "navigatePrevious", "clobberSettingsKey", "passwordInput", "shortcutKeyEnabled", - "hasShortcutKey", "languageSwitchKeyEnabled", "isMultiLine", "tw", "th", - "keys"); - public static void mainKeyboardView_setKeyboard(final Keyboard keyboard, - final int orientation) { - final KeyboardId kid = keyboard.mId; - final boolean isPasswordView = kid.passwordInput(); - final ResearchLogger researchLogger = getInstance(); - researchLogger.setIsPasswordView(isPasswordView); - researchLogger.enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD, - KeyboardId.elementIdToName(kid.mElementId), - kid.mLocale + ":" + kid.mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET), - orientation, kid.mWidth, KeyboardId.modeName(kid.mMode), kid.imeAction(), - kid.navigateNext(), kid.navigatePrevious(), kid.mClobberSettingsKey, - isPasswordView, kid.mShortcutKeyEnabled, kid.mHasShortcutKey, - kid.mLanguageSwitchKeyEnabled, kid.isMultiLine(), keyboard.mOccupiedWidth, - keyboard.mOccupiedHeight, keyboard.getKeys()); - } - - /** - * Log a call to LatinIME.revertCommit(). - * - * SystemResponse: The IME has reverted commited text. This happens when the user enters - * a word, commits it by pressing space or punctuation, and then reverts the commit by hitting - * backspace. - */ - private static final LogStatement LOGSTATEMENT_LATINIME_REVERTCOMMIT = - new LogStatement("LatinIMERevertCommit", true, false, "committedWord", - "originallyTypedWord", "separatorString"); - public static void latinIME_revertCommit(final String committedWord, - final String originallyTypedWord, final boolean isBatchMode, - final String separatorString) { - // TODO: Prioritize adding a unit test for this method (as it is especially complex) - // TODO: Update the UserRecording LogBuffer as well as the MainLogBuffer - final ResearchLogger researchLogger = getInstance(); - // - // 1. Remove separator LogUnit - final LogUnit lastLogUnit = researchLogger.mMainLogBuffer.peekLastLogUnit(); - // Check that we're not at the beginning of input - if (lastLogUnit == null) return; - // Check that we're after a separator - if (lastLogUnit.getWordsAsString() != null) return; - // Remove separator - final LogUnit separatorLogUnit = researchLogger.mMainLogBuffer.unshiftIn(); - - // 2. Add revert LogStatement - final LogUnit revertedLogUnit = researchLogger.mMainLogBuffer.peekLastLogUnit(); - if (revertedLogUnit == null) return; - if (!revertedLogUnit.getWordsAsString().equals(scrubDigitsFromString(committedWord))) { - // Any word associated with the reverted LogUnit has already had its digits scrubbed, so - // any digits in the committedWord argument must also be scrubbed for an accurate - // comparison. - return; - } - researchLogger.enqueueEvent(revertedLogUnit, LOGSTATEMENT_LATINIME_REVERTCOMMIT, - committedWord, originallyTypedWord, separatorString); - - // 3. Update the word associated with the LogUnit - revertedLogUnit.setWords(originallyTypedWord); - revertedLogUnit.setContainsUserDeletions(); - - // 4. Re-add the separator LogUnit - researchLogger.mMainLogBuffer.shiftIn(separatorLogUnit); - - // 5. Record stats - researchLogger.mStatistics.recordRevertCommit(SystemClock.uptimeMillis()); - } - - /** - * Log a call to PointerTracker.callListenerOnCancelInput(). - * - * UserAction: The user has canceled the input, e.g., by pressing down, but then removing - * outside the keyboard area. - * TODO: Verify - */ - private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCANCELINPUT = - new LogStatement("PointerTrackerCallListenerOnCancelInput", false, false); - public static void pointerTracker_callListenerOnCancelInput() { - getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCANCELINPUT); - } - - /** - * Log a call to PointerTracker.callListenerOnCodeInput(). - * - * SystemResponse: The user has entered a key through the normal tapping mechanism. - * LatinIME.onCodeInput will also be called. - */ - private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT = - new LogStatement("PointerTrackerCallListenerOnCodeInput", true, false, "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 ResearchLogger researchLogger = getInstance(); - researchLogger.enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT, - Constants.printableCode(scrubDigitFromCodePoint(code)), - outputText == null ? null : scrubDigitsFromString(outputText.toString()), - x, y, ignoreModifierKey, altersCode, key.isEnabled()); - } - } - - /** - * Log a call to PointerTracker.callListenerCallListenerOnRelease(). - * - * UserAction: The user has released their finger or thumb from the screen. - */ - private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONRELEASE = - new LogStatement("PointerTrackerCallListenerOnRelease", true, false, "code", - "withSliding", "ignoreModifierKey", "isEnabled"); - public static void pointerTracker_callListenerOnRelease(final Key key, final int primaryCode, - final boolean withSliding, final boolean ignoreModifierKey) { - if (key != null) { - getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONRELEASE, - Constants.printableCode(scrubDigitFromCodePoint(primaryCode)), withSliding, - ignoreModifierKey, key.isEnabled()); - } - } - - /** - * Log a call to PointerTracker.onDownEvent(). - * - * UserAction: The user has pressed down on a key. - * TODO: Differentiate with LatinIME.processMotionEvent. - */ - private static final LogStatement LOGSTATEMENT_POINTERTRACKER_ONDOWNEVENT = - new LogStatement("PointerTrackerOnDownEvent", true, false, "deltaT", "distanceSquared"); - public static void pointerTracker_onDownEvent(long deltaT, int distanceSquared) { - getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_ONDOWNEVENT, deltaT, - distanceSquared); - } - - /** - * Log a call to PointerTracker.onMoveEvent(). - * - * UserAction: The user has moved their finger while pressing on the screen. - * TODO: Differentiate with LatinIME.processMotionEvent(). - */ - private static final LogStatement LOGSTATEMENT_POINTERTRACKER_ONMOVEEVENT = - new LogStatement("PointerTrackerOnMoveEvent", true, false, "x", "y", "lastX", "lastY"); - public static void pointerTracker_onMoveEvent(final int x, final int y, final int lastX, - final int lastY) { - getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_ONMOVEEVENT, x, y, lastX, lastY); - } - - /** - * Log a call to RichInputConnection.commitCompletion(). - * - * SystemResponse: The IME has committed a completion. A completion is an application- - * specific suggestion that is presented in a pop-up menu in the TextView. - */ - private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_COMMITCOMPLETION = - new LogStatement("RichInputConnectionCommitCompletion", true, false, "completionInfo"); - public static void richInputConnection_commitCompletion(final CompletionInfo completionInfo) { - final ResearchLogger researchLogger = getInstance(); - researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_COMMITCOMPLETION, - completionInfo); - } - - /** - * Log a call to RichInputConnection.revertDoubleSpacePeriod(). - * - * SystemResponse: The IME has reverted ". ", which had previously replaced two typed spaces. - */ - private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_REVERTDOUBLESPACEPERIOD = - new LogStatement("RichInputConnectionRevertDoubleSpacePeriod", false, false); - public static void richInputConnection_revertDoubleSpacePeriod() { - final ResearchLogger researchLogger = getInstance(); - // An extra LogUnit is added for the period; this is removed here because of the revert. - researchLogger.uncommitCurrentLogUnit(null, true /* dumpCurrentLogUnit */); - // TODO: This will probably be lost as the user backspaces further. Figure out how to put - // it into the right logUnit. - researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_REVERTDOUBLESPACEPERIOD); - } - - /** - * Log a call to RichInputConnection.revertSwapPunctuation(). - * - * SystemResponse: The IME has reverted a punctuation swap. - */ - private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_REVERTSWAPPUNCTUATION = - new LogStatement("RichInputConnectionRevertSwapPunctuation", false, false); - public static void richInputConnection_revertSwapPunctuation() { - getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_REVERTSWAPPUNCTUATION); - } - - /** - * Log a call to LatinIME.commitCurrentAutoCorrection(). - * - * SystemResponse: The IME has committed an auto-correction. An auto-correction changes the raw - * text input to another word (or words) that the user more likely desired to type. - */ - private static final LogStatement LOGSTATEMENT_LATINIME_COMMITCURRENTAUTOCORRECTION = - new LogStatement("LatinIMECommitCurrentAutoCorrection", true, true, "typedWord", - "autoCorrection", "separatorString"); - public static void latinIme_commitCurrentAutoCorrection(final String typedWord, - final String autoCorrection, final String separatorString, final boolean isBatchMode, - final SuggestedWords suggestedWords) { - final String scrubbedTypedWord = scrubDigitsFromString(typedWord); - final String scrubbedAutoCorrection = scrubDigitsFromString(autoCorrection); - final ResearchLogger researchLogger = getInstance(); - researchLogger.mCurrentLogUnit.initializeSuggestions(suggestedWords); - researchLogger.onWordFinished(scrubbedAutoCorrection, isBatchMode); - - // Add the autocorrection logStatement at the end of the logUnit for the committed word. - // We have to do this after calling commitCurrentLogUnitAsWord, because it may split the - // current logUnit, and then we have to peek to get the logUnit reference back. - final LogUnit logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit(); - // TODO: Add test to confirm that the commitCurrentAutoCorrection log statement should - // always be added to logUnit (if non-null) and not mCurrentLogUnit. - researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_COMMITCURRENTAUTOCORRECTION, - scrubbedTypedWord, scrubbedAutoCorrection, separatorString); - } - - private boolean isExpectingCommitText = false; - - /** - * Log a call to RichInputConnection.commitText(). - * - * SystemResponse: The IME is committing text. This happens after the user has typed a word - * and then a space or punctuation key. - */ - private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTIONCOMMITTEXT = - new LogStatement("RichInputConnectionCommitText", true, false, "newCursorPosition"); - public static void richInputConnection_commitText(final String committedWord, - final int newCursorPosition, final boolean isBatchMode) { - final ResearchLogger researchLogger = getInstance(); - // Only include opening and closing logSegments if private data is included - final String scrubbedWord = scrubDigitsFromString(committedWord); - if (!researchLogger.isExpectingCommitText) { - researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTIONCOMMITTEXT, - newCursorPosition); - researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, Long.MAX_VALUE, isBatchMode); - } - researchLogger.isExpectingCommitText = false; - } - - /** - * Shared events for logging committed text. - * - * The "CommitTextEventHappened" LogStatement is written to the log even if privacy rules - * indicate that the word contents should not be logged. It has no contents, and only serves to - * record the event and thereby make it easier to calculate word-level statistics even when the - * word contents are unknown. - */ - private static final LogStatement LOGSTATEMENT_COMMITTEXT = - new LogStatement("CommitText", true /* isPotentiallyPrivate */, - false /* isPotentiallyRevealing */, "committedText", "isBatchMode"); - private static final LogStatement LOGSTATEMENT_COMMITTEXT_EVENT_HAPPENED = - new LogStatement("CommitTextEventHappened", false /* isPotentiallyPrivate */, - false /* isPotentiallyRevealing */); - private void enqueueCommitText(final String word, final boolean isBatchMode) { - // Event containing the word; will be published only if privacy checks pass - enqueueEvent(LOGSTATEMENT_COMMITTEXT, word, isBatchMode); - // Event not containing the word; will always be published - enqueueEvent(LOGSTATEMENT_COMMITTEXT_EVENT_HAPPENED); - } - - /** - * Log a call to RichInputConnection.deleteSurroundingText(). - * - * SystemResponse: The IME has deleted text. - */ - private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT = - new LogStatement("RichInputConnectionDeleteSurroundingText", true, false, - "beforeLength", "afterLength"); - public static void richInputConnection_deleteSurroundingText(final int beforeLength, - final int afterLength) { - getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT, - beforeLength, afterLength); - } - - /** - * Log a call to RichInputConnection.finishComposingText(). - * - * SystemResponse: The IME has left the composing text as-is. - */ - private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT = - new LogStatement("RichInputConnectionFinishComposingText", false, false); - public static void richInputConnection_finishComposingText() { - getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT); - } - - /** - * Log a call to RichInputConnection.performEditorAction(). - * - * SystemResponse: The IME is invoking an action specific to the editor. - */ - private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_PERFORMEDITORACTION = - new LogStatement("RichInputConnectionPerformEditorAction", false, false, - "imeActionId"); - public static void richInputConnection_performEditorAction(final int imeActionId) { - getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_PERFORMEDITORACTION, - imeActionId); - } - - /** - * Log a call to RichInputConnection.sendKeyEvent(). - * - * SystemResponse: The IME is telling the TextView that a key is being pressed through an - * alternate channel. - * TODO: only for hardware keys? - */ - private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SENDKEYEVENT = - new LogStatement("RichInputConnectionSendKeyEvent", true, false, "eventTime", "action", - "code"); - public static void richInputConnection_sendKeyEvent(final KeyEvent keyEvent) { - getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SENDKEYEVENT, - keyEvent.getEventTime(), keyEvent.getAction(), keyEvent.getKeyCode()); - } - - /** - * Log a call to RichInputConnection.setComposingText(). - * - * SystemResponse: The IME is setting the composing text. Happens each time a character is - * entered. - */ - private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SETCOMPOSINGTEXT = - new LogStatement("RichInputConnectionSetComposingText", true, true, "text", - "newCursorPosition"); - public static void richInputConnection_setComposingText(final CharSequence text, - final int newCursorPosition) { - if (text == null) { - throw new RuntimeException("setComposingText is null"); - } - getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SETCOMPOSINGTEXT, text, - newCursorPosition); - } - - /** - * Log a call to RichInputConnection.setSelection(). - * - * SystemResponse: The IME is requesting that the selection change. User-initiated selection- - * change requests do not go through this method -- it's only when the system wants to change - * the selection. - */ - private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SETSELECTION = - new LogStatement("RichInputConnectionSetSelection", true, false, "from", "to"); - public static void richInputConnection_setSelection(final int from, final int to) { - getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SETSELECTION, from, to); - } - - /** - * Log a call to SuddenJumpingTouchEventHandler.onTouchEvent(). - * - * SystemResponse: The IME has filtered input events in case of an erroneous sensor reading. - */ - private static final LogStatement LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT = - new LogStatement("SuddenJumpingTouchEventHandlerOnTouchEvent", true, false, - "motionEvent"); - public static void suddenJumpingTouchEventHandler_onTouchEvent(final MotionEvent me) { - if (me != null) { - getInstance().enqueueEvent(LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT, - MotionEvent.obtain(me)); - } - } - - /** - * Log a call to SuggestionsView.setSuggestions(). - * - * SystemResponse: The IME is setting the suggestions in the suggestion strip. - */ - private static final LogStatement LOGSTATEMENT_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS = - new LogStatement("SuggestionStripViewSetSuggestions", true, true, "suggestedWords"); - public static void suggestionStripView_setSuggestions(final SuggestedWords suggestedWords) { - if (suggestedWords != null) { - getInstance().enqueueEvent(LOGSTATEMENT_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS, - suggestedWords); - } - } - - /** - * The user has indicated a particular point in the log that is of interest. - * - * UserAction: From direct menu invocation. - */ - private static final LogStatement LOGSTATEMENT_USER_TIMESTAMP = - new LogStatement("UserTimestamp", false, false); - public void userTimestamp() { - getInstance().enqueueEvent(LOGSTATEMENT_USER_TIMESTAMP); - } - - /** - * Log a call to LatinIME.onEndBatchInput(). - * - * SystemResponse: The system has completed a gesture. - */ - private static final LogStatement LOGSTATEMENT_LATINIME_ONENDBATCHINPUT = - new LogStatement("LatinIMEOnEndBatchInput", true, false, "enteredText", - "enteredWordPos", "suggestedWords"); - public static void latinIME_onEndBatchInput(final CharSequence enteredText, - final int enteredWordPos, final SuggestedWords suggestedWords) { - final ResearchLogger researchLogger = getInstance(); - if (!TextUtils.isEmpty(enteredText) && hasLetters(enteredText.toString())) { - researchLogger.mCurrentLogUnit.setWords(enteredText.toString()); - } - researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONENDBATCHINPUT, enteredText, - enteredWordPos, suggestedWords); - researchLogger.mCurrentLogUnit.initializeSuggestions(suggestedWords); - researchLogger.mStatistics.recordGestureInput(enteredText.length(), - SystemClock.uptimeMillis()); - } - - private static final LogStatement LOGSTATEMENT_LATINIME_HANDLEBACKSPACE = - new LogStatement("LatinIMEHandleBackspace", true, false, "numCharacters"); - /** - * Log a call to LatinIME.handleBackspace() that is not a batch delete. - * - * UserInput: The user is deleting one or more characters by hitting the backspace key once. - * The covers single character deletes as well as deleting selections. - * - * @param numCharacters how many characters the backspace operation deleted - * @param shouldUncommitLogUnit whether to uncommit the last {@code LogUnit} in the - * {@code LogBuffer} - */ - public static void latinIME_handleBackspace(final int numCharacters, - final boolean shouldUncommitLogUnit) { - final ResearchLogger researchLogger = getInstance(); - researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_HANDLEBACKSPACE, numCharacters); - if (shouldUncommitLogUnit) { - ResearchLogger.getInstance().uncommitCurrentLogUnit( - null, true /* dumpCurrentLogUnit */); - } - } - - /** - * Log a call to LatinIME.handleBackspace() that is a batch delete. - * - * UserInput: The user is deleting a gestured word by hitting the backspace key once. - */ - private static final LogStatement LOGSTATEMENT_LATINIME_HANDLEBACKSPACE_BATCH = - new LogStatement("LatinIMEHandleBackspaceBatch", true, false, "deletedText", - "numCharacters"); - public static void latinIME_handleBackspace_batch(final CharSequence deletedText, - final int numCharacters) { - final ResearchLogger researchLogger = getInstance(); - researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_HANDLEBACKSPACE_BATCH, deletedText, - numCharacters); - researchLogger.mStatistics.recordGestureDelete(deletedText.length(), - SystemClock.uptimeMillis()); - researchLogger.uncommitCurrentLogUnit(deletedText.toString(), - false /* dumpCurrentLogUnit */); - } - - /** - * Log a long interval between user operation. - * - * UserInput: The user has not done anything for a while. - */ - private static final LogStatement LOGSTATEMENT_ONUSERPAUSE = new LogStatement("OnUserPause", - false, false, "intervalInMs"); - public static void onUserPause(final long interval) { - final ResearchLogger researchLogger = getInstance(); - researchLogger.enqueueEvent(LOGSTATEMENT_ONUSERPAUSE, interval); - } - - /** - * Record the current time in case the LogUnit is later split. - * - * If the current logUnit is split, then tapping, motion events, etc. before this time should - * be assigned to one LogUnit, and events after this time should go into the following LogUnit. - */ - public static void recordTimeForLogUnitSplit() { - final ResearchLogger researchLogger = getInstance(); - researchLogger.setSavedDownEventTime(SystemClock.uptimeMillis()); - researchLogger.mSavedDownEventTime = Long.MAX_VALUE; - } - - /** - * Log a call to LatinIME.handleSeparator() - * - * SystemResponse: The system is inserting a separator character, possibly performing auto- - * correction or other actions appropriate at the end of a word. - */ - private static final LogStatement LOGSTATEMENT_LATINIME_HANDLESEPARATOR = - new LogStatement("LatinIMEHandleSeparator", false, false, "primaryCode", - "isComposingWord"); - public static void latinIME_handleSeparator(final int primaryCode, - final boolean isComposingWord) { - final ResearchLogger researchLogger = getInstance(); - researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_HANDLESEPARATOR, primaryCode, - isComposingWord); - } - - /** - * Call this method when the logging system has attempted publication of an n-gram. - * - * Statistics are gathered about the success or failure. - * - * @param publishabilityResultCode a result code as defined by - * {@code MainLogBuffer.PUBLISHABILITY_*} - */ - static void recordPublishabilityResultCode(final int publishabilityResultCode) { - final ResearchLogger researchLogger = getInstance(); - final Statistics statistics = researchLogger.mStatistics; - statistics.recordPublishabilityResultCode(publishabilityResultCode); - } - - /** - * Log statistics. - * - * ContextualData, recorded at the end of a session. - */ - private static final LogStatement LOGSTATEMENT_STATISTICS = - new LogStatement("Statistics", false, false, "charCount", "letterCount", "numberCount", - "spaceCount", "deleteOpsCount", "wordCount", "isEmptyUponStarting", - "isEmptinessStateKnown", "averageTimeBetweenKeys", "averageTimeBeforeDelete", - "averageTimeDuringRepeatedDelete", "averageTimeAfterDelete", - "dictionaryWordCount", "splitWordsCount", "gestureInputCount", - "gestureCharsCount", "gesturesDeletedCount", "manualSuggestionsCount", - "revertCommitsCount", "correctedWordsCount", "autoCorrectionsCount", - "publishableCount", "unpublishableStoppingCount", - "unpublishableIncorrectWordCount", "unpublishableSampledTooRecentlyCount", - "unpublishableDictionaryUnavailableCount", "unpublishableMayContainDigitCount", - "unpublishableNotInDictionaryCount"); - private static void logStatistics() { - final ResearchLogger researchLogger = getInstance(); - final Statistics statistics = researchLogger.mStatistics; - researchLogger.enqueueEvent(LOGSTATEMENT_STATISTICS, 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(), - statistics.mDictionaryWordCount, statistics.mSplitWordsCount, - statistics.mGesturesInputCount, statistics.mGesturesCharsCount, - statistics.mGesturesDeletedCount, statistics.mManualSuggestionsCount, - statistics.mRevertCommitsCount, statistics.mCorrectedWordsCount, - statistics.mAutoCorrectionsCount, statistics.mPublishableCount, - statistics.mUnpublishableStoppingCount, statistics.mUnpublishableIncorrectWordCount, - statistics.mUnpublishableSampledTooRecently, - statistics.mUnpublishableDictionaryUnavailable, - statistics.mUnpublishableMayContainDigit, statistics.mUnpublishableNotInDictionary); - } -} diff --git a/java/src/com/android/inputmethod/research/ResearchSettings.java b/java/src/com/android/inputmethod/research/ResearchSettings.java deleted file mode 100644 index c0bc03fde..000000000 --- a/java/src/com/android/inputmethod/research/ResearchSettings.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.research; - -import android.content.SharedPreferences; - -import java.util.UUID; - -public final class ResearchSettings { - public static final String PREF_RESEARCH_LOGGER_UUID = "pref_research_logger_uuid"; - public static final String PREF_RESEARCH_LOGGER_ENABLED_FLAG = - "pref_research_logger_enabled_flag"; - public static final String PREF_RESEARCH_LOGGER_HAS_SEEN_SPLASH = - "pref_research_logger_has_seen_splash"; - public static final String PREF_RESEARCH_LAST_DIR_CLEANUP_TIME = - "pref_research_last_dir_cleanup_time"; - - private ResearchSettings() { - // Intentional empty constructor for singleton. - } - - public static String readResearchLoggerUuid(final SharedPreferences prefs) { - if (prefs.contains(PREF_RESEARCH_LOGGER_UUID)) { - return prefs.getString(PREF_RESEARCH_LOGGER_UUID, null); - } - // Generate a random string as uuid if not yet set - final String newUuid = UUID.randomUUID().toString(); - prefs.edit().putString(PREF_RESEARCH_LOGGER_UUID, newUuid).apply(); - return newUuid; - } - - public static boolean readResearchLoggerEnabledFlag(final SharedPreferences prefs) { - return prefs.getBoolean(PREF_RESEARCH_LOGGER_ENABLED_FLAG, false); - } - - public static void writeResearchLoggerEnabledFlag(final SharedPreferences prefs, - final boolean isEnabled) { - prefs.edit().putBoolean(PREF_RESEARCH_LOGGER_ENABLED_FLAG, isEnabled).apply(); - } - - public static boolean readHasSeenSplash(final SharedPreferences prefs) { - return prefs.getBoolean(PREF_RESEARCH_LOGGER_HAS_SEEN_SPLASH, false); - } - - public static void writeHasSeenSplash(final SharedPreferences prefs, - final boolean hasSeenSplash) { - prefs.edit().putBoolean(PREF_RESEARCH_LOGGER_HAS_SEEN_SPLASH, hasSeenSplash).apply(); - } - - public static long readResearchLastDirCleanupTime(final SharedPreferences prefs) { - return prefs.getLong(PREF_RESEARCH_LAST_DIR_CLEANUP_TIME, 0L); - } - - public static void writeResearchLastDirCleanupTime(final SharedPreferences prefs, - final long lastDirCleanupTime) { - prefs.edit().putLong(PREF_RESEARCH_LAST_DIR_CLEANUP_TIME, lastDirCleanupTime).apply(); - } -} diff --git a/java/src/com/android/inputmethod/research/Statistics.java b/java/src/com/android/inputmethod/research/Statistics.java deleted file mode 100644 index fd323a104..000000000 --- a/java/src/com/android/inputmethod/research/Statistics.java +++ /dev/null @@ -1,279 +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.research; - -import android.util.Log; - -import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.define.ProductionFlag; - -import java.util.concurrent.TimeUnit; - -public class Statistics { - private static final String TAG = Statistics.class.getSimpleName(); - private static final boolean DEBUG = false - && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; - - // TODO: Cleanup comments to only including those giving meaningful information. - // 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; - // Number of words found in the dictionary. - int mDictionaryWordCount; - // Number of words split and spaces automatically entered. - int mSplitWordsCount; - // Number of words entered during a session. - int mCorrectedWordsCount; - // Number of gestures that were input. - int mGesturesInputCount; - // Number of gestures that were deleted. - int mGesturesDeletedCount; - // Total number of characters in words entered by gesture. - int mGesturesCharsCount; - // Number of manual suggestions chosen. - int mManualSuggestionsCount; - // Number of times that autocorrection was invoked. - int mAutoCorrectionsCount; - // Number of times a commit was reverted in this session. - int mRevertCommitsCount; - // Whether the text field was empty upon editing - boolean mIsEmptyUponStarting; - boolean mIsEmptinessStateKnown; - - // Counts of how often an n-gram is collected or not, and the reasons for the decision. - // Keep consistent with publishability result code list in MainLogBuffer - int mPublishableCount; - int mUnpublishableStoppingCount; - int mUnpublishableIncorrectWordCount; - int mUnpublishableSampledTooRecently; - int mUnpublishableDictionaryUnavailable; - int mUnpublishableMayContainDigit; - int mUnpublishableNotInDictionary; - - // 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 long MIN_TYPING_INTERMISSION = TimeUnit.SECONDS.toMillis(2); - public static final long MIN_DELETION_INTERMISSION = TimeUnit.SECONDS.toMillis(10); - - // The last time that a tap was performed - private long mLastTapTime; - // 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; - mDictionaryWordCount = 0; - mSplitWordsCount = 0; - mCorrectedWordsCount = 0; - mGesturesInputCount = 0; - mGesturesDeletedCount = 0; - mManualSuggestionsCount = 0; - mRevertCommitsCount = 0; - mAutoCorrectionsCount = 0; - mIsEmptyUponStarting = true; - mIsEmptinessStateKnown = false; - mKeyCounter.reset(); - mBeforeDeleteKeyCounter.reset(); - mDuringRepeatedDeleteKeysCounter.reset(); - mAfterDeleteKeyCounter.reset(); - mGesturesCharsCount = 0; - mGesturesDeletedCount = 0; - mPublishableCount = 0; - mUnpublishableStoppingCount = 0; - mUnpublishableIncorrectWordCount = 0; - mUnpublishableSampledTooRecently = 0; - mUnpublishableDictionaryUnavailable = 0; - mUnpublishableMayContainDigit = 0; - mUnpublishableNotInDictionary = 0; - - mLastTapTime = 0; - mIsLastKeyDeleteKey = false; - } - - public void recordChar(int codePoint, long time) { - if (DEBUG) { - Log.d(TAG, "recordChar() called"); - } - if (codePoint == Constants.CODE_DELETE) { - mDeleteKeyCount++; - recordUserAction(time, true /* isDeletion */); - } else { - mCharCount++; - if (Character.isDigit(codePoint)) { - mNumberCount++; - } - if (Character.isLetter(codePoint)) { - mLetterCount++; - } - if (Character.isSpaceChar(codePoint)) { - mSpaceCount++; - } - recordUserAction(time, false /* isDeletion */); - } - } - - public void recordWordEntered(final boolean isDictionaryWord, - final boolean containsCorrection) { - mWordCount++; - if (isDictionaryWord) { - mDictionaryWordCount++; - } - if (containsCorrection) { - mCorrectedWordsCount++; - } - } - - public void recordSplitWords() { - mSplitWordsCount++; - } - - public void recordGestureInput(final int numCharsEntered, final long time) { - mGesturesInputCount++; - mGesturesCharsCount += numCharsEntered; - recordUserAction(time, false /* isDeletion */); - } - - public void setIsEmptyUponStarting(final boolean isEmpty) { - mIsEmptyUponStarting = isEmpty; - mIsEmptinessStateKnown = true; - } - - public void recordGestureDelete(final int length, final long time) { - mGesturesDeletedCount++; - recordUserAction(time, true /* isDeletion */); - } - - public void recordManualSuggestion(final long time) { - mManualSuggestionsCount++; - recordUserAction(time, false /* isDeletion */); - } - - public void recordAutoCorrection(final long time) { - mAutoCorrectionsCount++; - recordUserAction(time, false /* isDeletion */); - } - - public void recordRevertCommit(final long time) { - mRevertCommitsCount++; - recordUserAction(time, true /* isDeletion */); - } - - private void recordUserAction(final long time, final boolean isDeletion) { - final long delta = time - mLastTapTime; - if (isDeletion) { - if (delta < MIN_DELETION_INTERMISSION) { - if (mIsLastKeyDeleteKey) { - mDuringRepeatedDeleteKeysCounter.add(delta); - } else { - mBeforeDeleteKeyCounter.add(delta); - } - } else { - ResearchLogger.onUserPause(delta); - } - } else { - if (mIsLastKeyDeleteKey && delta < MIN_DELETION_INTERMISSION) { - mAfterDeleteKeyCounter.add(delta); - } else if (!mIsLastKeyDeleteKey && delta < MIN_TYPING_INTERMISSION) { - mKeyCounter.add(delta); - } else { - ResearchLogger.onUserPause(delta); - } - } - mIsLastKeyDeleteKey = isDeletion; - mLastTapTime = time; - } - - public void recordPublishabilityResultCode(final int publishabilityResultCode) { - // Keep consistent with publishability result code list in MainLogBuffer - switch (publishabilityResultCode) { - case MainLogBuffer.PUBLISHABILITY_PUBLISHABLE: - mPublishableCount++; - break; - case MainLogBuffer.PUBLISHABILITY_UNPUBLISHABLE_STOPPING: - mUnpublishableStoppingCount++; - break; - case MainLogBuffer.PUBLISHABILITY_UNPUBLISHABLE_INCORRECT_WORD_COUNT: - mUnpublishableIncorrectWordCount++; - break; - case MainLogBuffer.PUBLISHABILITY_UNPUBLISHABLE_SAMPLED_TOO_RECENTLY: - mUnpublishableSampledTooRecently++; - break; - case MainLogBuffer.PUBLISHABILITY_UNPUBLISHABLE_DICTIONARY_UNAVAILABLE: - mUnpublishableDictionaryUnavailable++; - break; - case MainLogBuffer.PUBLISHABILITY_UNPUBLISHABLE_MAY_CONTAIN_DIGIT: - mUnpublishableMayContainDigit++; - break; - case MainLogBuffer.PUBLISHABILITY_UNPUBLISHABLE_NOT_IN_DICTIONARY: - mUnpublishableNotInDictionary++; - break; - } - } -} diff --git a/java/src/com/android/inputmethod/research/Uploader.java b/java/src/com/android/inputmethod/research/Uploader.java deleted file mode 100644 index c7ea3e69d..000000000 --- a/java/src/com/android/inputmethod/research/Uploader.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.research; - -import android.Manifest; -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.text.TextUtils; -import android.util.Log; - -import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.define.ProductionFlag; - -import java.io.BufferedReader; -import java.io.File; -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; - -/** - * Manages the uploading of ResearchLog files. - */ -public final class Uploader { - private static final String TAG = Uploader.class.getSimpleName(); - private static final boolean DEBUG = false - && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; - // Set IS_INHIBITING_AUTO_UPLOAD to true for local testing - private static final boolean IS_INHIBITING_UPLOAD = false - && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; - private static final int BUF_SIZE = 1024 * 8; - - private final Context mContext; - private final ResearchLogDirectory mResearchLogDirectory; - private final URL mUrl; - - public Uploader(final Context context) { - mContext = context; - mResearchLogDirectory = new ResearchLogDirectory(context); - - final String urlString = context.getString(R.string.research_logger_upload_url); - if (TextUtils.isEmpty(urlString)) { - mUrl = null; - return; - } - URL url = null; - try { - url = new URL(urlString); - } catch (final MalformedURLException e) { - Log.e(TAG, "Bad URL for uploading", e); - } - mUrl = url; - } - - public boolean isPossibleToUpload() { - return hasUploadingPermission() && mUrl != null && !IS_INHIBITING_UPLOAD; - } - - private boolean hasUploadingPermission() { - final PackageManager packageManager = mContext.getPackageManager(); - return packageManager.checkPermission(Manifest.permission.INTERNET, - mContext.getPackageName()) == PackageManager.PERMISSION_GRANTED; - } - - public boolean isConvenientToUpload() { - return isExternallyPowered() && hasWifiConnection(); - } - - private boolean isExternallyPowered() { - final Intent intent = mContext.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) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); - final NetworkInfo wifiInfo = manager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); - return wifiInfo.isConnected(); - } - - public void doUpload() { - final File[] files = mResearchLogDirectory.getUploadableLogFiles(); - if (files == null) return; - for (final File file : files) { - uploadFile(file); - } - } - - private void uploadFile(final File file) { - if (DEBUG) { - Log.d(TAG, "attempting upload of " + file.getAbsolutePath()); - } - 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 outputStream = connection.getOutputStream(); - uploadContents(fileInputStream, outputStream); - if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { - Log.d(TAG, "upload failed: " + connection.getResponseCode()); - final InputStream netInputStream = connection.getInputStream(); - final BufferedReader reader = new BufferedReader(new InputStreamReader( - netInputStream)); - String line; - while ((line = reader.readLine()) != null) { - Log.d(TAG, "| " + reader.readLine()); - } - reader.close(); - return; - } - file.delete(); - if (DEBUG) { - Log.d(TAG, "upload successful"); - } - } catch (final IOException e) { - Log.e(TAG, "Exception uploading file", e); - } finally { - if (fileInputStream != null) { - try { - fileInputStream.close(); - } catch (final IOException e) { - Log.e(TAG, "Exception closing uploaded file", e); - } - } - if (connection != null) { - connection.disconnect(); - } - } - } - - private static void uploadContents(final InputStream is, final OutputStream os) - throws IOException { - // TODO: Switch to NIO. - final byte[] buf = new byte[BUF_SIZE]; - int numBytesRead; - while ((numBytesRead = is.read(buf)) != -1) { - os.write(buf, 0, numBytesRead); - } - } -} diff --git a/java/src/com/android/inputmethod/research/UploaderService.java b/java/src/com/android/inputmethod/research/UploaderService.java deleted file mode 100644 index fd3f2f60e..000000000 --- a/java/src/com/android/inputmethod/research/UploaderService.java +++ /dev/null @@ -1,88 +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.research; - -import android.app.AlarmManager; -import android.app.IntentService; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.os.SystemClock; - -/** - * Service to invoke the uploader. - * - * Can be regularly invoked, invoked on boot, etc. - */ -public final class UploaderService extends IntentService { - private static final String TAG = UploaderService.class.getSimpleName(); - public static final long RUN_INTERVAL = AlarmManager.INTERVAL_HOUR; - public static final String EXTRA_UPLOAD_UNCONDITIONALLY = UploaderService.class.getName() - + ".extra.UPLOAD_UNCONDITIONALLY"; - - public UploaderService() { - super("Research Uploader Service"); - } - - @Override - protected void onHandleIntent(final Intent intent) { - // We may reach this point either because the alarm fired, or because the system explicitly - // requested that an Upload occur. In the latter case, we want to cancel the alarm in case - // it's about to fire. - cancelAndRescheduleUploadingService(this, false /* needsRescheduling */); - - final Uploader uploader = new Uploader(this); - if (!uploader.isPossibleToUpload()) return; - if (isUploadingUnconditionally(intent.getExtras()) || uploader.isConvenientToUpload()) { - uploader.doUpload(); - } - cancelAndRescheduleUploadingService(this, true /* needsRescheduling */); - } - - private boolean isUploadingUnconditionally(final Bundle bundle) { - if (bundle == null) return false; - if (bundle.containsKey(EXTRA_UPLOAD_UNCONDITIONALLY)) { - return bundle.getBoolean(EXTRA_UPLOAD_UNCONDITIONALLY); - } - return false; - } - - /** - * Arrange for the UploaderService to be run on a regular basis. - * - * Any existing scheduled invocation of UploaderService is removed and optionally rescheduled. - * This may cause problems if this method is called so often that no scheduled invocation is - * ever run. But if the delay is short enough that it will go off when the user is sleeping, - * then there should be no starvation. - * - * @param context {@link Context} object - * @param needsRescheduling whether to schedule a future intent to be delivered to this service - */ - public static void cancelAndRescheduleUploadingService(final Context context, - final boolean needsRescheduling) { - final Intent intent = new Intent(context, UploaderService.class); - final PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0); - final AlarmManager alarmManager = (AlarmManager) context.getSystemService( - Context.ALARM_SERVICE); - alarmManager.cancel(pendingIntent); - if (needsRescheduling) { - alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() - + UploaderService.RUN_INTERVAL, pendingIntent); - } - } -} diff --git a/java/src/com/android/inputmethod/research/ui/SplashScreen.java b/java/src/com/android/inputmethod/research/ui/SplashScreen.java deleted file mode 100644 index 78ed668d1..000000000 --- a/java/src/com/android/inputmethod/research/ui/SplashScreen.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.research.ui; - -import android.app.AlertDialog.Builder; -import android.app.Dialog; -import android.content.DialogInterface; -import android.content.DialogInterface.OnCancelListener; -import android.content.Intent; -import android.inputmethodservice.InputMethodService; -import android.net.Uri; -import android.os.IBinder; -import android.view.Window; -import android.view.WindowManager.LayoutParams; - -import com.android.inputmethod.latin.R.string; - -/** - * Show a dialog when the user first opens the keyboard. - * - * The splash screen is a modal dialog box presented when the user opens this keyboard for the first - * time. It is useful for giving specific warnings that must be shown to the user before use. - * - * While the splash screen does share with the setup wizard the common goal of presenting - * information to the user before use, they are presented at different times and with different - * capabilities. The setup wizard is launched by tapping on the icon, and walks the user through - * the setup process. It can, however, be bypassed by enabling the keyboard from Settings directly. - * The splash screen cannot be bypassed, and is therefore more appropriate for obtaining user - * consent. - */ -public class SplashScreen { - public interface UserConsentListener { - public void onSplashScreenUserClickedOk(); - } - - final UserConsentListener mListener; - final Dialog mSplashDialog; - - public SplashScreen(final InputMethodService inputMethodService, - final UserConsentListener listener) { - mListener = listener; - final Builder builder = new Builder(inputMethodService) - .setTitle(string.research_splash_title) - .setMessage(string.research_splash_content) - .setPositiveButton(android.R.string.yes, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - mListener.onSplashScreenUserClickedOk(); - mSplashDialog.dismiss(); - } - }) - .setNegativeButton(android.R.string.no, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - final String packageName = inputMethodService.getPackageName(); - final Uri packageUri = Uri.parse("package:" + packageName); - final Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, - packageUri); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - inputMethodService.startActivity(intent); - } - }) - .setCancelable(true) - .setOnCancelListener( - new OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - inputMethodService.requestHideSelf(0); - } - }); - mSplashDialog = builder.create(); - } - - /** - * Show the splash screen. - * - * The user must consent to the terms presented in the SplashScreen before they can use the - * keyboard. If they cancel instead, they are given the option to uninstall the keybard. - * - * @param windowToken {@link IBinder} to attach dialog to - */ - public void showSplashScreen(final IBinder windowToken) { - final Window window = mSplashDialog.getWindow(); - final LayoutParams lp = window.getAttributes(); - lp.token = windowToken; - lp.type = LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; - window.setAttributes(lp); - window.addFlags(LayoutParams.FLAG_ALT_FOCUSABLE_IM); - mSplashDialog.show(); - } - - public boolean isShowing() { - return mSplashDialog.isShowing(); - } -} |