diff options
Diffstat (limited to 'java/src')
34 files changed, 1407 insertions, 632 deletions
diff --git a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java index 2e6649bf2..0499a3456 100644 --- a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java +++ b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java @@ -189,9 +189,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; } diff --git a/java/src/com/android/inputmethod/event/CombinerChain.java b/java/src/com/android/inputmethod/event/CombinerChain.java index 8b59dc52a..990f7deea 100644 --- a/java/src/com/android/inputmethod/event/CombinerChain.java +++ b/java/src/com/android/inputmethod/event/CombinerChain.java @@ -23,6 +23,7 @@ import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.utils.CollectionUtils; import java.util.ArrayList; +import java.util.HashMap; /** * This class implements the logic chain between receiving events and generating code points. @@ -43,6 +44,13 @@ public class CombinerChain { private SpannableStringBuilder mStateFeedback; private final ArrayList<Combiner> mCombiners; + private static final HashMap<String, Class> IMPLEMENTED_COMBINERS + = new HashMap<String, Class>(); + static { + IMPLEMENTED_COMBINERS.put("MyanmarReordering", MyanmarReordering.class); + } + private static final String COMBINER_SPEC_SEPARATOR = ";"; + /** * Create an combiner chain. * @@ -56,6 +64,9 @@ public class CombinerChain { mCombiners = CollectionUtils.newArrayList(); // 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(); mStateFeedback = new SpannableStringBuilder(); } @@ -114,4 +125,29 @@ public class CombinerChain { 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 combinerClass = IMPLEMENTED_COMBINERS.get(combinerDescriptor); + if (null == combinerClass) { + throw new RuntimeException("Unknown combiner descriptor: " + combinerDescriptor); + } + try { + combiners[i++] = (Combiner)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/MyanmarReordering.java b/java/src/com/android/inputmethod/event/MyanmarReordering.java new file mode 100644 index 000000000..da0228bd2 --- /dev/null +++ b/java/src/com/android/inputmethod/event/MyanmarReordering.java @@ -0,0 +1,259 @@ +/* + * 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 com.android.inputmethod.latin.utils.CollectionUtils; + +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 = CollectionUtils.newArrayList(); + + // 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/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java index 816a94300..4c2250740 100644 --- a/java/src/com/android/inputmethod/keyboard/Key.java +++ b/java/src/com/android/inputmethod/keyboard/Key.java @@ -218,7 +218,7 @@ public class Key implements Comparable<Key> { * * @param keySpec the key specification. * @param keyAttr the Key XML attributes array. - * @param keyStyle the {@link KeyStyle} of this key. + * @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. @@ -857,17 +857,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 @@ -880,25 +869,43 @@ 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 { diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java index 1cd6ef249..fc9faa6e3 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java @@ -28,6 +28,7 @@ import android.view.inputmethod.EditorInfo; 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; @@ -64,7 +65,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { * what user actually typed. */ private boolean mIsAutoCorrectionActive; - private KeyboardTheme mKeyboardTheme = KeyboardTheme.getDefaultKeyboardTheme(); + private KeyboardTheme mKeyboardTheme; private Context mThemeContext; private static final KeyboardSwitcher sInstance = new KeyboardSwitcher(); @@ -101,7 +102,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { 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(); @@ -253,10 +254,11 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { // Implements {@link KeyboardState.SwitchActions}. @Override public void setEmojiKeyboard() { + final Keyboard keyboard = mKeyboardView.getKeyboard(); mMainKeyboardFrame.setVisibility(View.GONE); mEmojiPalettesView.startEmojiPalettes( mKeyboardTextsSet.getText(KeyboardTextsSet.SWITCH_TO_ALPHA_KEY_LABEL), - mKeyboardView.getKeyVisualAttribute()); + mKeyboardView.getKeyVisualAttribute(), keyboard.mIconsSet); mEmojiPalettesView.setVisibility(View.VISIBLE); } @@ -342,7 +344,8 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { 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); diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardTheme.java b/java/src/com/android/inputmethod/keyboard/KeyboardTheme.java index 4db72ad4d..e0b74fa14 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardTheme.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardTheme.java @@ -17,34 +17,85 @@ 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.latin.R; -import com.android.inputmethod.latin.settings.Settings; + +import java.util.Arrays; +import java.util.Comparator; public final class KeyboardTheme { private static final String TAG = KeyboardTheme.class.getSimpleName(); - public static final int THEME_ID_ICS = 0; - public static final int THEME_ID_KLP = 2; - private static final int DEFAULT_THEME_ID = THEME_ID_KLP; + static final String KITKAT_KEYBOARD_THEME_KEY = "pref_keyboard_layout_20110916"; + static final String KEYBOARD_THEME_KEY = "pref_keyboard_theme_20140509"; + + static final int THEME_ID_ICS = 0; + static final int THEME_ID_KLP = 2; + static final int THEME_ID_LXX = 3; + static final int DEFAULT_THEME_ID = THEME_ID_KLP; private static final KeyboardTheme[] KEYBOARD_THEMES = { - new KeyboardTheme(THEME_ID_ICS, R.style.KeyboardTheme_ICS), - new KeyboardTheme(THEME_ID_KLP, R.style.KeyboardTheme_KLP), + new KeyboardTheme(THEME_ID_ICS, R.style.KeyboardTheme_ICS, + VERSION_CODES.ICE_CREAM_SANDWICH), + new KeyboardTheme(THEME_ID_KLP, R.style.KeyboardTheme_KLP, + VERSION_CODES.KITKAT), + new KeyboardTheme(THEME_ID_LXX, R.style.KeyboardTheme_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, new Comparator<KeyboardTheme>() { + @Override + public int compare(final KeyboardTheme lhs, final KeyboardTheme rhs) { + if (lhs.mMinApiVersion > rhs.mMinApiVersion) return -1; + if (lhs.mMinApiVersion < rhs.mMinApiVersion) return 1; + return 0; + } + }); + } public final int mThemeId; public final int mStyleId; + final int mMinApiVersion; // Note: The themeId should be aligned with "themeId" attribute of Keyboard style - // in values/style.xml. - public KeyboardTheme(final int themeId, final int styleId) { + // in values/themes-<style>.xml. + private KeyboardTheme(final int themeId, final int styleId, final int minApiVersion) { mThemeId = themeId; mStyleId = styleId; + mMinApiVersion = minApiVersion; + } + + @Override + public boolean equals(final Object o) { + if (o == this) return true; + return (o instanceof KeyboardTheme) && ((KeyboardTheme)o).mThemeId == mThemeId; } - private static KeyboardTheme searchKeyboardTheme(final int themeId) { + @Override + public int hashCode() { + return mThemeId; + } + + // TODO: This method should be removed when {@link LatinImeLogger} is removed. + public int getCompatibleThemeIdForLogging() { + switch (mThemeId) { + case THEME_ID_ICS: + return 5; + case THEME_ID_KLP: + return 9; + case THEME_ID_LXX: + return 10; + default: // Invalid theme + return -1; + } + } + + private 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) { @@ -54,18 +105,57 @@ public final class KeyboardTheme { return null; } - public static KeyboardTheme getDefaultKeyboardTheme() { - return searchKeyboardTheme(DEFAULT_THEME_ID); + 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 && Build.VERSION.CODENAME.startsWith("L")) { + return VERSION_CODES.CUR_DEVELOPMENT; + } + return sdkVersion; + } + + static KeyboardTheme getDefaultKeyboardTheme(final SharedPreferences prefs, + final int sdkVersion) { + final String obsoleteIdString = prefs.getString(KITKAT_KEYBOARD_THEME_KEY, null); + if (obsoleteIdString != null) { + // Remove old preference. + prefs.edit().remove(KITKAT_KEYBOARD_THEME_KEY).apply(); + if (sdkVersion <= VERSION_CODES.KITKAT) { + try { + final int themeId = Integer.parseInt(obsoleteIdString); + final KeyboardTheme theme = searchKeyboardThemeById(themeId); + if (theme != null) { + return theme; + } + Log.w(TAG, "Unknown keyboard theme in preference: " + obsoleteIdString); + } catch (final NumberFormatException e) { + Log.w(TAG, "Illegal keyboard theme in preference: " + obsoleteIdString); + } + } + } + // 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) { + prefs.edit().putString(KEYBOARD_THEME_KEY, themeIdString).apply(); } public static KeyboardTheme getKeyboardTheme(final SharedPreferences prefs) { - final String themeIdString = prefs.getString(Settings.PREF_KEYBOARD_LAYOUT, null); + final int sdkVersion = getSdkVersion(); + final String themeIdString = prefs.getString(KEYBOARD_THEME_KEY, null); if (themeIdString == null) { - return getDefaultKeyboardTheme(); + return getDefaultKeyboardTheme(prefs, sdkVersion); } try { final int themeId = Integer.parseInt(themeIdString); - final KeyboardTheme theme = searchKeyboardTheme(themeId); + final KeyboardTheme theme = searchKeyboardThemeById(themeId); if (theme != null) { return theme; } @@ -73,9 +163,8 @@ public final class KeyboardTheme { } catch (final NumberFormatException e) { Log.w(TAG, "Illegal keyboard theme in preference: " + themeIdString); } - // Reset preference to default value. - final String defaultThemeIdString = Integer.toString(DEFAULT_THEME_ID); - prefs.edit().putString(Settings.PREF_KEYBOARD_LAYOUT, defaultThemeIdString).apply(); - return getDefaultKeyboardTheme(); + // Remove preference. + prefs.edit().remove(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 8ca00b005..a6eac4cd7 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java @@ -47,6 +47,8 @@ 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_keyLabelHorizontalPadding * @attr ref R.styleable#KeyboardView_keyHintLetterPadding * @attr ref R.styleable#KeyboardView_keyPopupHintLetterPadding @@ -81,7 +83,10 @@ 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 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"; @@ -124,6 +129,14 @@ 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; mKeyLabelHorizontalPadding = keyboardViewAttr.getDimensionPixelOffset( R.styleable.KeyboardView_keyLabelHorizontalPadding, 0); mKeyHintLetterPadding = keyboardViewAttr.getDimension( @@ -133,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(); @@ -323,7 +336,9 @@ public class KeyboardView extends View { params.mAnimAlpha = Constants.Color.ALPHA_OPAQUE; if (!key.isSpacer()) { - onDrawKeyBackground(key, canvas, mKeyBackground); + final Drawable background = key.selectBackgroundDrawable( + mKeyBackground, mFunctionalKeyBackground, mSpacebarBackground); + onDrawKeyBackground(key, canvas, background); } onDrawKeyTopVisuals(key, canvas, paint, params); @@ -338,8 +353,6 @@ public class KeyboardView extends View { final int bgHeight = key.getHeight() + padding.top + padding.bottom; final int bgX = -padding.left; final int bgY = -padding.top; - final int[] drawableState = key.getCurrentDrawableState(); - background.setState(drawableState); final Rect bounds = background.getBounds(); if (bgWidth != bounds.right || bgHeight != bounds.bottom) { background.setBounds(0, 0, bgWidth, bgHeight); @@ -414,18 +427,23 @@ public class KeyboardView extends View { } } - 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) { diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java index 8f79a9128..4a0976845 100644 --- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java @@ -74,8 +74,8 @@ import java.util.WeakHashMap; * @attr ref R.styleable#MainKeyboardView_autoCorrectionSpacebarLedIcon * @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_spacebarBackground * @attr ref R.styleable#MainKeyboardView_languageOnSpacebarFinalAlpha * @attr ref R.styleable#MainKeyboardView_languageOnSpacebarFadeoutAnimator * @attr ref R.styleable#MainKeyboardView_altCodeKeyWhileTypingFadeoutAnimator @@ -119,7 +119,6 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack /* Space key and its icon and background. */ private Key mSpaceKey; private Drawable mSpacebarIcon; - private final Drawable mSpacebarBackground; // Stuff to draw language name on spacebar. private final int mLanguageOnSpacebarFinalAlpha; private ObjectAnimator mLanguageOnSpacebarFadeoutAnimator; @@ -129,7 +128,9 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack 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. @@ -151,7 +152,6 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack private final SlidingKeyInputDrawingPreview mSlidingKeyInputDrawingPreview; // Key preview - private static final boolean FADE_OUT_KEY_TOP_LETTER_WHEN_KEY_IS_PRESSED = false; private final KeyPreviewDrawParams mKeyPreviewDrawParams; private final KeyPreviewChoreographer mKeyPreviewChoreographer; @@ -221,8 +221,6 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack R.styleable.MainKeyboardView_backgroundDimAlpha, 0); mBackgroundDimAlphaPaint.setColor(Color.BLACK); mBackgroundDimAlphaPaint.setAlpha(backgroundDimAlpha); - mSpacebarBackground = mainKeyboardViewAttr.getDrawable( - R.styleable.MainKeyboardView_spacebarBackground); mAutoCorrectionSpacebarLedEnabled = mainKeyboardViewAttr.getBoolean( R.styleable.MainKeyboardView_autoCorrectionSpacebarLedEnabled, false); mAutoCorrectionSpacebarLedIcon = mainKeyboardViewAttr.getDrawable( @@ -231,6 +229,9 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack 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( @@ -551,6 +552,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack } // Note that this method is called from a non-UI thread. + @SuppressWarnings("static-method") public void setMainDictionaryAvailability(final boolean mainDictionaryAvailable) { PointerTracker.setMainDictionaryAvailability(mainDictionaryAvailable); } @@ -858,30 +860,12 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack } } - // Draw key background. - @Override - protected void onDrawKeyBackground(final Key key, final Canvas canvas, - final Drawable background) { - if (key.getCode() == Constants.CODE_SPACE) { - super.onDrawKeyBackground(key, canvas, mSpacebarBackground); - return; - } - super.onDrawKeyBackground(key, canvas, background); - } - @Override protected void onDrawKeyTopVisuals(final Key key, final Canvas canvas, final Paint paint, final KeyDrawParams params) { if (key.altCodeWhileTyping() && key.isEnabled()) { params.mAnimAlpha = mAltCodeKeyWhileTypingAnimAlpha; } - // Don't draw key top letter when key preview is showing. - if (FADE_OUT_KEY_TOP_LETTER_WHEN_KEY_IS_PRESSED - && mKeyPreviewChoreographer.isShowingKeyPreview(key)) { - // TODO: Fade out animation for the key top letter, and fade in animation for the key - // background color when the user presses the key. - return; - } final int code = key.getCode(); if (code == Constants.CODE_SPACE) { drawSpacebar(key, canvas, paint); @@ -948,12 +932,17 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack final float descent = paint.descent(); final float textHeight = -paint.ascent() + descent; final float baseline = height / 2 + textHeight / 2; - paint.setColor(mLanguageOnSpacebarTextShadowColor); - paint.setAlpha(mLanguageOnSpacebarAnimAlpha); - canvas.drawText(language, width / 2, baseline - descent - 1, paint); + 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); } // Draw the spacebar icon at the bottom diff --git a/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java b/java/src/com/android/inputmethod/keyboard/emoji/DynamicGridKeyboard.java index 67a222732..c7a9025c0 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java +++ b/java/src/com/android/inputmethod/keyboard/emoji/DynamicGridKeyboard.java @@ -14,13 +14,12 @@ * 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; @@ -36,7 +35,7 @@ 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; @@ -62,7 +61,7 @@ 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; } 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..dd0e3e838 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/emoji/EmojiCategory.java @@ -0,0 +1,358 @@ +/* + * 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.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 com.android.inputmethod.latin.utils.CollectionUtils; + +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[] sCategoryIcon = { + 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[] 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 = CollectionUtils.newHashMap(); + private final ArrayList<CategoryProperties> mShownCategories = + CollectionUtils.newArrayList(); + private final ConcurrentHashMap<Long, DynamicGridKeyboard> + mCategoryKeyboardMap = new ConcurrentHashMap<Long, DynamicGridKeyboard>(); + + private int mCurrentCategoryId = EmojiCategory.ID_UNSPECIFIED; + private int mCurrentCategoryPageId = 0; + + public EmojiCategory(final SharedPreferences prefs, final Resources res, + final KeyboardLayoutSet layoutSet) { + 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); + } + 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 getCategoryIcon(final int categoryId) { + return sCategoryIcon[categoryId]; + } + + public String getCategoryLabel(final int categoryId) { + return sCategoryLabel[categoryId]; + } + + public String getAccessibilityDescription(final int categoryId) { + return mRes.getString(sAccessibilityDescriptionResourceIdsForCategories[categoryId]); + } + + public ArrayList<CategoryProperties> getShownCategories() { + return mShownCategories; + } + + 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<Integer, Integer>(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 = CollectionUtils.newArrayList(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 d56a3cf25..74cfd9b4b 100644 --- a/java/src/com/android/inputmethod/keyboard/EmojiCategoryPageIndicatorView.java +++ b/java/src/com/android/inputmethod/keyboard/emoji/EmojiCategoryPageIndicatorView.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.inputmethod.keyboard; +package com.android.inputmethod.keyboard.emoji; import com.android.inputmethod.latin.R; @@ -24,8 +24,8 @@ import android.graphics.Paint; import android.util.AttributeSet; import android.widget.LinearLayout; -public class EmojiCategoryPageIndicatorView extends LinearLayout { - private static final float BOTTOM_MARGIN_RATIO = 0.66f; +public final class EmojiCategoryPageIndicatorView extends LinearLayout { + private static final float BOTTOM_MARGIN_RATIO = 1.0f; private final Paint mPaint = new Paint(); private int mCategoryPageSize = 0; private int mCurrentCategoryPageId = 0; diff --git a/java/src/com/android/inputmethod/keyboard/internal/EmojiLayoutParams.java b/java/src/com/android/inputmethod/keyboard/emoji/EmojiLayoutParams.java index d57ea5a94..77c183a99 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/EmojiLayoutParams.java +++ b/java/src/com/android/inputmethod/keyboard/emoji/EmojiLayoutParams.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.inputmethod.keyboard.internal; +package com.android.inputmethod.keyboard.emoji; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.utils.ResourceUtils; @@ -24,7 +24,7 @@ import android.support.v4.view.ViewPager; import android.widget.ImageView; import android.widget.LinearLayout; -public class EmojiLayoutParams { +final class EmojiLayoutParams { private static final int DEFAULT_KEYBOARD_ROWS = 4; public final int mEmojiPagerHeight; diff --git a/java/src/com/android/inputmethod/keyboard/internal/EmojiPageKeyboardView.java b/java/src/com/android/inputmethod/keyboard/emoji/EmojiPageKeyboardView.java index e175a051e..d14ffeef9 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/EmojiPageKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/emoji/EmojiPageKeyboardView.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.inputmethod.keyboard.internal; +package com.android.inputmethod.keyboard.emoji; import android.content.Context; import android.os.Handler; @@ -26,15 +26,14 @@ import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.KeyDetector; import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardView; -import com.android.inputmethod.keyboard.PointerTracker; import com.android.inputmethod.latin.R; /** * This is an extended {@link KeyboardView} class that hosts an emoji page keyboard. - * Multi-touch unsupported. No {@link PointerTracker}s. No gesture support. + * Multi-touch unsupported. No gesture support. */ // TODO: Implement key popup preview. -public final class EmojiPageKeyboardView extends KeyboardView implements +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 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..52a4dde97 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/emoji/EmojiPalettesAdapter.java @@ -0,0 +1,146 @@ +/* + * 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; +import com.android.inputmethod.latin.utils.CollectionUtils; + +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 = + CollectionUtils.newSparseArray(); + 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() { + // Make sure the delayed key-down event (highlight effect and haptic feedback) will be + // canceled. + final EmojiPageKeyboardView currentKeyboardView = + mActiveKeyboardViews.get(mActivePosition); + if (currentKeyboardView != null) { + currentKeyboardView.releaseCurrentKey(); + } + } + + @Override + public int getCount() { + return mEmojiCategory.getTotalPageCountOfAllCategories(); + } + + @Override + public void setPrimaryItem(final ViewGroup container, final int position, + final Object object) { + if (mActivePosition == position) { + return; + } + final EmojiPageKeyboardView 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 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/EmojiPalettesView.java b/java/src/com/android/inputmethod/keyboard/emoji/EmojiPalettesView.java index d8b5758a6..3813c578a 100644 --- a/java/src/com/android/inputmethod/keyboard/EmojiPalettesView.java +++ b/java/src/com/android/inputmethod/keyboard/emoji/EmojiPalettesView.java @@ -14,55 +14,44 @@ * limitations under the License. */ -package com.android.inputmethod.keyboard; +package com.android.inputmethod.keyboard.emoji; 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.Color; -import android.graphics.Rect; -import android.os.Build; +import android.graphics.Typeface; import android.os.CountDownTimer; import android.preference.PreferenceManager; -import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager; import android.util.AttributeSet; -import android.util.Log; import android.util.Pair; -import android.util.SparseArray; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; -import android.view.ViewGroup; +import android.widget.ImageButton; 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.EmojiLayoutParams; -import com.android.inputmethod.keyboard.internal.EmojiPageKeyboardView; +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.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.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; /** @@ -79,17 +68,17 @@ import java.util.concurrent.TimeUnit; public final class EmojiPalettesView extends LinearLayout implements OnTabChangeListener, ViewPager.OnPageChangeListener, View.OnClickListener, View.OnTouchListener, EmojiPageKeyboardView.OnKeyEventListener { - static final String TAG = EmojiPalettesView.class.getSimpleName(); - private static final boolean DEBUG_PAGER = false; - private final int mKeyBackgroundId; - private final int mEmojiFunctionalKeyBackgroundId; + private final int mFunctionalKeyBackgroundId; + private final int mSpacebarBackgroundId; private final ColorStateList mTabLabelColor; private final DeleteKeyOnTouchListener mDeleteKeyOnTouchListener; private EmojiPalettesAdapter mEmojiPalettesAdapter; private final EmojiLayoutParams mEmojiLayoutParams; + private ImageButton mDeleteKey; private TextView mAlphabetKeyLeft; private TextView mAlphabetKeyRight; + private ImageButton mSpacebar; private TabHost mTabHost; private ViewPager mEmojiPager; private int mCurrentPagerPosition = 0; @@ -97,317 +86,6 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange 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 = { - 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[] 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 = 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; - 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); - } - 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(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 getCategoryIcon(final int categoryId) { - return sCategoryIcon[categoryId]; - } - - public String getCategoryLabel(final int categoryId) { - return sCategoryLabel[categoryId]; - } - - public String getAccessibilityDescription(final int categoryId) { - return mRes.getString(sAccessibilityDescriptionResourceIdsForCategories[categoryId]); - } - - public ArrayList<CategoryProperties> getShownCategories() { - return mShownCategories; - } - - 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 == CATEGORY_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(CATEGORY_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<Integer, Integer>(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 == CATEGORY_ID_RECENTS) { - final DynamicGridKeyboard kbd = new DynamicGridKeyboard(mPrefs, - mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS), - mMaxPageKeyCount, categoryId); - mCategoryKeyboardMap.put(categotyKeyboardMapKey, kbd); - return kbd; - } - - final Keyboard keyboard = mLayoutSet.getKeyboard(sCategoryElementId[categoryId]); - final Key[][] sortedKeys = sortKeysIntoPages( - keyboard.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 = CollectionUtils.newArrayList(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; - } - } - private final EmojiCategory mEmojiCategory; public EmojiPalettesView(final Context context, final AttributeSet attrs) { @@ -418,10 +96,12 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange super(context, attrs, defStyle); final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs, R.styleable.KeyboardView, defStyle, R.style.KeyboardView); - mKeyBackgroundId = keyboardViewAttr.getResourceId( + final int keyBackgroundId = keyboardViewAttr.getResourceId( R.styleable.KeyboardView_keyBackground, 0); - mEmojiFunctionalKeyBackgroundId = keyboardViewAttr.getResourceId( - R.styleable.KeyboardView_keyBackgroundEmojiFunctional, 0); + mFunctionalKeyBackgroundId = keyboardViewAttr.getResourceId( + R.styleable.KeyboardView_functionalKeyBackground, keyBackgroundId); + mSpacebarBackgroundId = keyboardViewAttr.getResourceId( + R.styleable.KeyboardView_spacebarBackground, keyBackgroundId); keyboardViewAttr.recycle(); final TypedArray emojiPalettesViewAttr = context.obtainStyledAttributes(attrs, R.styleable.EmojiPalettesView, defStyle, R.style.EmojiPalettesView); @@ -470,8 +150,11 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange final TextView textView = (TextView)LayoutInflater.from(getContext()).inflate( R.layout.emoji_keyboard_tab_label, null); textView.setText(mEmojiCategory.getCategoryLabel(categoryId)); + textView.setTypeface(Typeface.DEFAULT_BOLD); textView.setContentDescription(mEmojiCategory.getAccessibilityDescription(categoryId)); - textView.setTextColor(mTabLabelColor); + if (mTabLabelColor != null) { + textView.setTextColor(mTabLabelColor); + } tspec.setIndicator(textView); } host.addTab(tspec); @@ -481,7 +164,8 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange protected void onFinishInflate() { mTabHost = (TabHost)findViewById(R.id.emoji_category_tabhost); mTabHost.setup(); - for (final CategoryProperties properties : mEmojiCategory.getShownCategories()) { + for (final EmojiCategory.CategoryProperties properties + : mEmojiCategory.getShownCategories()) { addTab(mTabHost, properties.mCategoryId); } mTabHost.setOnTabChangedListener(this); @@ -506,9 +190,10 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange mEmojiLayoutParams.setActionBarProperties(actionBar); // deleteKey depends only on OnTouchListener. - final ImageView deleteKey = (ImageView)findViewById(R.id.emoji_keyboard_delete); - deleteKey.setTag(Constants.CODE_DELETE); - deleteKey.setOnTouchListener(mDeleteKeyOnTouchListener); + 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}. @@ -518,21 +203,21 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange // The text on alphabet keys are set at // {@link #startEmojiPalettes(String,int,float,Typeface)}. mAlphabetKeyLeft = (TextView)findViewById(R.id.emoji_keyboard_alphabet_left); - mAlphabetKeyLeft.setBackgroundResource(mEmojiFunctionalKeyBackgroundId); + mAlphabetKeyLeft.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(mEmojiFunctionalKeyBackgroundId); + mAlphabetKeyRight.setBackgroundResource(mFunctionalKeyBackgroundId); mAlphabetKeyRight.setTag(Constants.CODE_ALPHA_FROM_EMOJI); mAlphabetKeyRight.setOnTouchListener(this); mAlphabetKeyRight.setOnClickListener(this); - final ImageView spaceKey = (ImageView)findViewById(R.id.emoji_keyboard_space); - spaceKey.setBackgroundResource(mKeyBackgroundId); - spaceKey.setTag(Constants.CODE_SPACE); - spaceKey.setOnTouchListener(this); - spaceKey.setOnClickListener(this); - mEmojiLayoutParams.setKeyProperties(spaceKey); + mSpacebar = (ImageButton)findViewById(R.id.emoji_keyboard_space); + mSpacebar.setBackgroundResource(mSpacebarBackgroundId); + mSpacebar.setTag(Constants.CODE_SPACE); + mSpacebar.setOnTouchListener(this); + mSpacebar.setOnClickListener(this); + mEmojiLayoutParams.setKeyProperties(mSpacebar); } @Override @@ -631,7 +316,7 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange /** * Called from {@link EmojiPageKeyboardView} through - * {@link com.android.inputmethod.keyboard.internal.EmojiPageKeyboardView.OnKeyEventListener} + * {@link com.android.inputmethod.keyboard.emoji.EmojiPageKeyboardView.OnKeyEventListener} * interface to handle touch events from non-View-based elements such as Emoji buttons. */ @Override @@ -642,7 +327,7 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange /** * Called from {@link EmojiPageKeyboardView} through - * {@link com.android.inputmethod.keyboard.internal.EmojiPageKeyboardView.OnKeyEventListener} + * {@link com.android.inputmethod.keyboard.emoji.EmojiPageKeyboardView.OnKeyEventListener} * interface to handle touch events from non-View-based elements such as Emoji buttons. */ @Override @@ -674,10 +359,9 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange } public void startEmojiPalettes(final String switchToAlphaLabel, - final KeyVisualAttributes keyVisualAttr) { - if (DEBUG_PAGER) { - Log.d(TAG, "allocate emoji palettes memory " + mCurrentPagerPosition); - } + final KeyVisualAttributes keyVisualAttr, final KeyboardIconsSet iconSet) { + mDeleteKey.setImageDrawable(iconSet.getIconDrawable(KeyboardIconsSet.NAME_DELETE_KEY)); + mSpacebar.setImageDrawable(iconSet.getIconDrawable(KeyboardIconsSet.NAME_SPACE_KEY)); final KeyDrawParams params = new KeyDrawParams(); params.updateParams(mEmojiLayoutParams.getActionBarHeight(), keyVisualAttr); setupAlphabetKey(mAlphabetKeyLeft, switchToAlphaLabel, params); @@ -687,9 +371,6 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange } public void stopEmojiPalettes() { - if (DEBUG_PAGER) { - Log.d(TAG, "deallocate emoji palettes memory"); - } mEmojiPalettesAdapter.flushPendingRecentKeys(); mEmojiPager.setAdapter(null); } @@ -714,7 +395,7 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange return; } - if (oldCategoryId == CATEGORY_ID_RECENTS) { + 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. @@ -733,124 +414,11 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange } } - private static class EmojiPalettesAdapter extends PagerAdapter { - private final EmojiPageKeyboardView.OnKeyEventListener mListener; - private final DynamicGridKeyboard mRecentsKeyboard; - private final SparseArray<EmojiPageKeyboardView> mActiveKeyboardViews = - CollectionUtils.newSparseArray(); - private final EmojiCategory mEmojiCategory; - private int mActivePosition = 0; - - public EmojiPalettesAdapter(final EmojiCategory emojiCategory, - final EmojiPageKeyboardView.OnKeyEventListener 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(); - } - } - - public void onPageScrolled() { - // Make sure the delayed key-down event (highlight effect and haptic feedback) will be - // canceled. - final EmojiPageKeyboardView currentKeyboardView = - mActiveKeyboardViews.get(mActivePosition); - if (currentKeyboardView != null) { - currentKeyboardView.releaseCurrentKey(); - } - } - - @Override - public int getCount() { - return mEmojiCategory.getTotalPageCountOfAllCategories(); - } - - @Override - public void setPrimaryItem(final ViewGroup container, final int position, - final Object object) { - if (mActivePosition == position) { - return; - } - final EmojiPageKeyboardView 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 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); - } - } - } - private static class DeleteKeyOnTouchListener implements OnTouchListener { - private static final long MAX_REPEAT_COUNT_TIME = TimeUnit.SECONDS.toMillis(30); - private final int mDeleteKeyPressedBackgroundColor; - private final long mKeyRepeatStartTimeout; - private final long mKeyRepeatInterval; + static final long MAX_REPEAT_COUNT_TIME = TimeUnit.SECONDS.toMillis(30); + final int mDeleteKeyPressedBackgroundColor; + final long mKeyRepeatStartTimeout; + final long mKeyRepeatInterval; public DeleteKeyOnTouchListener(Context context) { final Resources res = context.getResources(); @@ -953,7 +521,7 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange } // Called by {@link #mTimer} in the UI thread as an auto key-repeat signal. - private void onKeyRepeat() { + void onKeyRepeat() { switch (mState) { case KEY_REPEAT_STATE_INITIALIZED: // Basically this should not happen. diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java index dfe0df04c..2aeeed87f 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java @@ -664,6 +664,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, @@ -675,10 +677,11 @@ public class KeyboardBuilder<KP extends KeyboardParams> { && passwordInputMatched && clobberSettingsKeyMatched && supportsSwitchingToShortcutImeMatched && hasShortcutKeyMatched && languageSwitchKeyEnabledMatched && isMultiLineMatched && imeActionMatched - && localeCodeMatched && languageCodeMatched && countryCodeMatched; + && isIconDefinedMatched && localeCodeMatched && languageCodeMatched + && countryCodeMatched; if (DEBUG) { - startTag("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s>%s", TAG_CASE, + startTag("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s>%s", TAG_CASE, textAttr(caseAttr.getString( R.styleable.Keyboard_Case_keyboardLayoutSet), "keyboardLayoutSet"), textAttr(caseAttr.getString( @@ -704,6 +707,8 @@ public class KeyboardBuilder<KP extends KeyboardParams> { "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 +760,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/KeyboardIconsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java index 6c9b5adc3..b5a94807e 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java @@ -42,7 +42,12 @@ public final class KeyboardIconsSet { 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"; @@ -64,7 +69,12 @@ public final class KeyboardIconsSet { 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, @@ -128,6 +138,10 @@ public final class KeyboardIconsSet { throw new RuntimeException("unknown icon name: " + name); } + public Drawable getIconDrawable(final String name) { + return getIconDrawable(getIconId(name)); + } + public Drawable getIconDrawable(final int iconId) { if (isValidIconId(iconId)) { return mIcons[iconId]; diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java index e71723a15..67ca59540 100644 --- a/java/src/com/android/inputmethod/latin/Constants.java +++ b/java/src/com/android/inputmethod/latin/Constants.java @@ -115,6 +115,11 @@ public final class Constants { */ 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. } @@ -164,6 +169,8 @@ public final class Constants { // 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}. diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java index 0742fbde9..cd380db6b 100644 --- a/java/src/com/android/inputmethod/latin/Dictionary.java +++ b/java/src/com/android/inputmethod/latin/Dictionary.java @@ -57,6 +57,8 @@ public abstract class Dictionary { public static final String TYPE_USER_HISTORY = "history"; // Personalization dictionary. public static final String TYPE_PERSONALIZATION = "personalization"; + // Contextual dictionary. + public static final String TYPE_CONTEXTUAL = "contextual"; public final String mDictType; public Dictionary(final String dictType) { diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java index 9735645cb..e0220e137 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java @@ -23,6 +23,7 @@ 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.ContextualDictionary; import com.android.inputmethod.latin.personalization.PersonalizationDictionary; import com.android.inputmethod.latin.personalization.UserHistoryDictionary; import com.android.inputmethod.latin.utils.CollectionUtils; @@ -55,7 +56,7 @@ public class DictionaryFacilitatorForSuggest { private boolean mIsUserDictEnabled = false; private volatile CountDownLatch mLatchForWaitingLoadingMainDictionary = new CountDownLatch(0); // To synchronize assigning mDictionaries to ensure closing dictionaries. - private Object mLock = new Object(); + private final Object mLock = new Object(); private static final String[] DICT_TYPES_ORDERED_TO_GET_SUGGESTION = new String[] { @@ -63,7 +64,8 @@ public class DictionaryFacilitatorForSuggest { Dictionary.TYPE_USER_HISTORY, Dictionary.TYPE_PERSONALIZATION, Dictionary.TYPE_USER, - Dictionary.TYPE_CONTACTS + Dictionary.TYPE_CONTACTS, + Dictionary.TYPE_CONTEXTUAL }; private static final Map<String, Class<? extends ExpandableBinaryDictionary>> @@ -74,6 +76,7 @@ public class DictionaryFacilitatorForSuggest { 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"; @@ -201,6 +204,7 @@ public class DictionaryFacilitatorForSuggest { if (usePersonalizedDicts) { subDictTypesToUse.add(Dictionary.TYPE_USER_HISTORY); subDictTypesToUse.add(Dictionary.TYPE_PERSONALIZATION); + subDictTypesToUse.add(Dictionary.TYPE_CONTEXTUAL); } final Dictionary newMainDict; @@ -366,32 +370,46 @@ public class DictionaryFacilitatorForSuggest { } public void addToUserHistory(final String suggestion, final boolean wasAutoCapitalized, - final String previousWord, final int timeStampInSeconds) { + final String previousWord, final int timeStampInSeconds, + final boolean blockPotentiallyOffensive) { final Dictionaries dictionaries = mDictionaries; + final String[] words = suggestion.split(Constants.WORD_SEPARATOR); + for (int i = 0; i < words.length; i++) { + final String currentWord = words[i]; + final String prevWord = (i == 0) ? previousWord : words[i - 1]; + final boolean wasCurrentWordAutoCapitalized = (i == 0) ? wasAutoCapitalized : false; + addWordToUserHistory(dictionaries, prevWord, currentWord, + wasCurrentWordAutoCapitalized, timeStampInSeconds, blockPotentiallyOffensive); + } + } + + private void addWordToUserHistory(final Dictionaries dictionaries, final String prevWord, + 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 = getMaxFrequency(suggestion); - if (maxFreq == 0) { + final int maxFreq = getMaxFrequency(word); + if (maxFreq == 0 && blockPotentiallyOffensive) { return; } - final String suggestionLowerCase = suggestion.toLowerCase(dictionaries.mLocale); + final String lowerCasedWord = word.toLowerCase(dictionaries.mLocale); final String secondWord; if (wasAutoCapitalized) { - if (isValidWord(suggestion, false /* ignoreCase */) - && !isValidWord(suggestionLowerCase, false /* ignoreCase */)) { + 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 = suggestion; + 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 = suggestionLowerCase; + secondWord = lowerCasedWord; } } else { // HACK: We'd like to avoid adding the capitalized form of common words to the User @@ -399,20 +417,20 @@ public class DictionaryFacilitatorForSuggest { // consolidation is done. // TODO: Remove this hack when ready. final int lowerCaseFreqInMainDict = dictionaries.hasDict(Dictionary.TYPE_MAIN) ? - dictionaries.getDict(Dictionary.TYPE_MAIN).getFrequency(suggestionLowerCase) : + 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 = suggestionLowerCase; + secondWord = lowerCasedWord; } else { - secondWord = suggestion; + 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, previousWord, secondWord, + UserHistoryDictionary.addToDictionary(userHistoryDictionary, prevWord, secondWord, isValid, timeStampInSeconds); } diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java index 6818c156e..e323f0ab2 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java @@ -607,6 +607,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { 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) { diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index d64a1a6f7..8a2ed1088 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -540,18 +540,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen refreshPersonalizationDictionarySession(); } - private DistracterFilter createDistracterFilter() { - final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); - // TODO: Create Keyboard when mainKeyboardView is null. - // TODO: Figure out the most reasonable keyboard for the filter. Refer to the - // spellchecker's logic. - final Keyboard keyboard = (mainKeyboardView != null) ? - mainKeyboardView.getKeyboard() : null; - final DistracterFilter distracterFilter = new DistracterFilter(mInputLogic.mSuggest, - keyboard); - return distracterFilter; - } - private void refreshPersonalizationDictionarySession() { final DictionaryFacilitatorForSuggest dictionaryFacilitator = mInputLogic.mSuggest.mDictionaryFacilitator; @@ -733,6 +721,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(); } @@ -808,7 +797,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // The app calling setText() has the effect of clearing the composing // span, so we should reset our state unconditionally, even if restarting is true. - mInputLogic.startInput(restarting, editorInfo); + // We also tell the input logic about the combining rules for the current subtype, so + // it can adjust its combiners if needed. + mInputLogic.startInput(restarting, editorInfo, + mSubtypeSwitcher.getCombiningRulesExtraValueOfCurrentSubtype()); // Note: the following does a round-trip IPC on the main thread: be careful final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); @@ -1751,6 +1743,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mInputLogic.mSuggest.mDictionaryFacilitator.clearPersonalizationDictionary(); } + @UsedForTesting + /* package for test */ DistracterFilter createDistracterFilter() { + return DistracterFilter.createDistracterFilter(mInputLogic.mSuggest, mKeyboardSwitcher); + } + public void dumpDictionaryForDebug(final String dictName) { final DictionaryFacilitatorForSuggest dictionaryFacilitator = mInputLogic.mSuggest.mDictionaryFacilitator; diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java index 021133945..c8a2fb2f9 100644 --- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java +++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java @@ -52,7 +52,6 @@ public final class SubtypeSwitcher { private /* final */ RichInputMethodManager mRichImm; private /* final */ Resources mResources; - private /* final */ ConnectivityManager mConnectivityManager; private final LanguageOnSpacebarHelper mLanguageOnSpacebarHelper = new LanguageOnSpacebarHelper(); @@ -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()); @@ -327,4 +326,8 @@ public final class SubtypeSwitcher { + DUMMY_EMOJI_SUBTYPE); return DUMMY_EMOJI_SUBTYPE; } + + public String getCombiningRulesExtraValueOfCurrentSubtype() { + return SubtypeLocaleUtils.getCombiningRulesExtraValue(getCurrentSubtype()); + } } diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java index d755195f2..cdee496a8 100644 --- a/java/src/com/android/inputmethod/latin/WordComposer.java +++ b/java/src/com/android/inputmethod/latin/WordComposer.java @@ -41,6 +41,7 @@ public final class WordComposer { public static final int CAPS_MODE_AUTO_SHIFT_LOCKED = 0x7; 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; @@ -91,6 +92,21 @@ public final class WordComposer { } /** + * Restart input with a new combining spec. + * @param combiningSpec The spec string for combining. This is found in the extra value. + */ + public void restart(final String combiningSpec) { + final String nonNullCombiningSpec = null == combiningSpec ? "" : combiningSpec; + if (nonNullCombiningSpec.equals(mCombiningSpec)) { + mCombinerChain.reset(); + } else { + mCombinerChain = new CombinerChain(CombinerChain.createCombiners(nonNullCombiningSpec)); + mCombiningSpec = nonNullCombiningSpec; + } + reset(); + } + + /** * Clear out the keys registered so far. */ public void reset() { diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java index 75432fbac..1156c7737 100644 --- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java @@ -97,6 +97,11 @@ public final class InputLogic { 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. + */ public InputLogic(final LatinIME latinIME, final SuggestionStripViewAccessor suggestionStripViewAccessor) { mLatinIME = latinIME; @@ -117,9 +122,12 @@ public final class InputLogic { * * @param restarting whether input is starting in the same field as before. Unused for now. * @param editorInfo the editorInfo associated with the editor. + * @param combiningSpec the combining spec string for this subtype */ - public void startInput(final boolean restarting, final EditorInfo editorInfo) { + public void startInput(final boolean restarting, final EditorInfo editorInfo, + final String combiningSpec) { mEnteredText = null; + mWordComposer.restart(combiningSpec); resetComposingState(true /* alsoResetLastComposedWord */); mDeleteCount = 0; mSpaceState = SpaceState.NONE; @@ -138,6 +146,14 @@ public final class InputLogic { } /** + * Call this when the subtype changes. + * @param combiningSpec the spec string for the combining rules + */ + public void onSubtypeChanged(final String combiningSpec) { + mWordComposer.restart(combiningSpec); + } + + /** * Clean up the input logic after input is finished. */ public void finishInput() { @@ -588,7 +604,7 @@ public final class InputLogic { if (null != candidate && mSuggestedWords.mSequenceNumber >= mAutoCommitSequenceNumber) { if (candidate.mSourceDict.shouldAutoCommit(candidate)) { - final String[] commitParts = candidate.mWord.split(" ", 2); + final String[] commitParts = candidate.mWord.split(Constants.WORD_SEPARATOR, 2); batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord); promotePhantomSpace(settingsValues); mConnection.commitText(commitParts[0], 0); @@ -920,7 +936,11 @@ public final class InputLogic { } else { mWordComposer.processEvent(inputTransaction.mEvent); } - mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); + if (mWordComposer.isComposingWord()) { + mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); + } else { + mConnection.commitText("", 1); + } inputTransaction.setRequiresUpdateSuggestions(); } else { if (mLastComposedWord.canRevertCommit()) { @@ -1225,7 +1245,7 @@ public final class InputLogic { final int timeStampInSeconds = (int)TimeUnit.MILLISECONDS.toSeconds( System.currentTimeMillis()); mSuggest.mDictionaryFacilitator.addToUserHistory(suggestion, wasAutoCapitalized, prevWord, - timeStampInSeconds); + timeStampInSeconds, settingsValues.mBlockPotentiallyOffensive); } public void performUpdateSuggestionStripSync(final SettingsValues settingsValues) { @@ -1946,10 +1966,11 @@ public final class InputLogic { final CharSequence chosenWordWithSuggestions = SuggestionSpanUtils.getTextWithSuggestionSpan(mLatinIME, chosenWord, suggestedWords); - mConnection.commitText(chosenWordWithSuggestions, 1); - // TODO: we pass 2 here, but would it be better to move this above and pass 1 instead? + // Use the 2nd previous word as the previous word because the 1st previous word is the word + // to be committed. final String prevWord = mConnection.getNthPreviousWord( settingsValues.mSpacingAndPunctuations, 2); + mConnection.commitText(chosenWordWithSuggestions, 1); // Add the word to the user history dictionary performAdditionToUserHistoryDictionary(settingsValues, chosenWord, prevWord); // TODO: figure out here if this is an auto-correct or if the best word is actually 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..96f03f9ff --- /dev/null +++ b/java/src/com/android/inputmethod/latin/personalization/ContextualDictionary.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.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 = PersonalizationDictionary.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) { + 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/settings/Settings.java b/java/src/com/android/inputmethod/latin/settings/Settings.java index a3aae8cb3..4e4c8885c 100644 --- a/java/src/com/android/inputmethod/latin/settings/Settings.java +++ b/java/src/com/android/inputmethod/latin/settings/Settings.java @@ -64,7 +64,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang "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 = diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java index 22cbd204c..e1d38e7c4 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java @@ -37,6 +37,7 @@ import android.util.Log; 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; @@ -253,11 +254,31 @@ public final class SettingsFragment extends InputMethodSettingsFragment } updateListPreferenceSummaryToCurrentValue(Settings.PREF_SHOW_SUGGESTIONS_SETTING); updateListPreferenceSummaryToCurrentValue(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY); - updateListPreferenceSummaryToCurrentValue(Settings.PREF_KEYBOARD_LAYOUT); + 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 public void onDestroy() { getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener( this); @@ -287,7 +308,7 @@ public final class SettingsFragment extends InputMethodSettingsFragment ensureConsistencyOfAutoCorrectionSettings(); updateListPreferenceSummaryToCurrentValue(Settings.PREF_SHOW_SUGGESTIONS_SETTING); updateListPreferenceSummaryToCurrentValue(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY); - updateListPreferenceSummaryToCurrentValue(Settings.PREF_KEYBOARD_LAYOUT); + updateListPreferenceSummaryToCurrentValue(Settings.PREF_KEYBOARD_THEME); refreshEnablingsOfKeypressSoundAndVibrationSettings(prefs, getResources()); } diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java index dde50ccaf..de2eb951e 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java @@ -205,7 +205,8 @@ public final class SettingsValues { } 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) { diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java index 8bfa63c3c..810bda758 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java +++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java @@ -381,6 +381,7 @@ final class SuggestionStripLayoutHelper { } // Disable this suggestion if the suggestion is null or empty. + // TODO: Fix disabled {@link TextView}'s content description. wordView.setEnabled(!TextUtils.isEmpty(word)); final CharSequence text = getEllipsizedText(word, width, wordView.getPaint()); final float scaleX = getTextScaleX(word, width, wordView.getPaint()); @@ -424,7 +425,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); diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java index a578fa4a4..619804afa 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java +++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java @@ -18,7 +18,9 @@ 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; @@ -31,6 +33,7 @@ import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.view.ViewGroup; import android.view.ViewParent; +import android.widget.ImageButton; import android.widget.RelativeLayout; import android.widget.TextView; @@ -59,12 +62,14 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick public void addWordToUserDictionary(String word); public void showImportantNoticeContents(); public void pickSuggestionManually(int index, SuggestedWordInfo word); + public void onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat); } 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; @@ -86,39 +91,42 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick private static class StripVisibilityGroup { private final View mSuggestionsStrip; + private final View mVoiceKey; private final View mAddToDictionaryStrip; private final View mImportantNoticeStrip; - public StripVisibilityGroup(final View suggestionsStrip, final View addToDictionaryStrip, - final View importantNoticeStrip) { + public StripVisibilityGroup(final View suggestionsStrip, final View voiceKey, + final View addToDictionaryStrip, final View importantNoticeStrip) { mSuggestionsStrip = suggestionsStrip; + mVoiceKey = voiceKey; mAddToDictionaryStrip = addToDictionaryStrip; mImportantNoticeStrip = importantNoticeStrip; - showSuggestionsStrip(); + showSuggestionsStrip(false /* voiceKeyEnabled */); } - public void setLayoutDirection(final boolean isRtlLanguage) { - final int layoutDirection = isRtlLanguage ? ViewCompat.LAYOUT_DIRECTION_RTL - : ViewCompat.LAYOUT_DIRECTION_LTR; + public void setLayoutDirection(final int layoutDirection) { ViewCompat.setLayoutDirection(mSuggestionsStrip, layoutDirection); ViewCompat.setLayoutDirection(mAddToDictionaryStrip, layoutDirection); ViewCompat.setLayoutDirection(mImportantNoticeStrip, layoutDirection); } - public void showSuggestionsStrip() { + public void showSuggestionsStrip(final boolean enableVoiceKey) { mSuggestionsStrip.setVisibility(VISIBLE); + mVoiceKey.setVisibility(enableVoiceKey ? VISIBLE : INVISIBLE); mAddToDictionaryStrip.setVisibility(INVISIBLE); mImportantNoticeStrip.setVisibility(INVISIBLE); } public void showAddToDictionaryStrip() { mSuggestionsStrip.setVisibility(INVISIBLE); + mVoiceKey.setVisibility(INVISIBLE); mAddToDictionaryStrip.setVisibility(VISIBLE); mImportantNoticeStrip.setVisibility(INVISIBLE); } - public void showImportantNoticeStrip() { + public void showImportantNoticeStrip(final boolean enableVoiceKey) { mSuggestionsStrip.setVisibility(INVISIBLE); + mVoiceKey.setVisibility(enableVoiceKey ? VISIBLE : INVISIBLE); mAddToDictionaryStrip.setVisibility(INVISIBLE); mImportantNoticeStrip.setVisibility(VISIBLE); } @@ -145,10 +153,11 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick inflater.inflate(R.layout.suggestions_strip, this); mSuggestionsStrip = (ViewGroup)findViewById(R.id.suggestions_strip); + 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(mSuggestionsStrip, mAddToDictionaryStrip, - mImportantNoticeStrip); + mStripVisibilityGroup = new StripVisibilityGroup(mSuggestionsStrip, mVoiceKey, + mAddToDictionaryStrip, mImportantNoticeStrip); for (int pos = 0; pos < SuggestedWords.MAX_SUGGESTIONS; pos++) { final TextView word = new TextView(context, null, R.attr.suggestionWordStyle); @@ -177,6 +186,13 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick 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); } /** @@ -188,16 +204,30 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick mMainKeyboardView = (MainKeyboardView)inputView.findViewById(R.id.keyboard_view); } + private boolean isVoiceKeyEnabled() { + if (mMainKeyboardView == null) { + return false; + } + final Keyboard keyboard = mMainKeyboardView.getKeyboard(); + if (keyboard == null) { + return false; + } + return keyboard.mId.mHasShortcutKey; + } + public void setSuggestions(final SuggestedWords suggestedWords, final boolean isRtlLanguage) { clear(); - mStripVisibilityGroup.setLayoutDirection(isRtlLanguage); + final int layoutDirection = isRtlLanguage ? ViewCompat.LAYOUT_DIRECTION_RTL + : ViewCompat.LAYOUT_DIRECTION_LTR; + setLayoutDirection(layoutDirection); + mStripVisibilityGroup.setLayoutDirection(layoutDirection); mSuggestedWords = suggestedWords; mSuggestionsCountInStrip = mLayoutHelper.layoutAndReturnSuggestionCountInStrip( mSuggestedWords, mSuggestionsStrip, this); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.suggestionStripView_setSuggestions(mSuggestedWords); } - mStripVisibilityGroup.showSuggestionsStrip(); + mStripVisibilityGroup.showSuggestionsStrip(isVoiceKeyEnabled()); } public int setMoreSuggestionsHeight(final int remainingHeight) { @@ -244,7 +274,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick dismissMoreSuggestionsPanel(); } mLayoutHelper.layoutImportantNotice(mImportantNoticeStrip, importantNoticeTitle); - mStripVisibilityGroup.showImportantNoticeStrip(); + mStripVisibilityGroup.showImportantNoticeStrip(isVoiceKeyEnabled()); mImportantNoticeStrip.setOnClickListener(this); return true; } @@ -252,7 +282,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick public void clear() { mSuggestionsStrip.removeAllViews(); removeAllDebugInfoViews(); - mStripVisibilityGroup.showSuggestionsStrip(); + mStripVisibilityGroup.showSuggestionsStrip(false /* enableVoiceKey */); dismissMoreSuggestionsPanel(); } @@ -415,6 +445,12 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick mListener.showImportantNoticeContents(); return; } + if (view == mVoiceKey) { + mListener.onCodeInput(Constants.CODE_SHORTCUT, + Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE, + false /* isKeyRepeat */); + return; + } final Object tag = view.getTag(); // {@link String} tag is set at {@link #showAddToDictionaryHint(String,CharSequence)}. if (tag instanceof String) { diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java index 48e43d6ef..55cbf79b3 100644 --- a/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java +++ b/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java @@ -17,21 +17,35 @@ package com.android.inputmethod.latin.utils; import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.keyboard.KeyboardSwitcher; +import com.android.inputmethod.keyboard.MainKeyboardView; +import com.android.inputmethod.latin.Constants; 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; /** - * This class is used to prevent distracters/misspellings being added to personalization + * This class is used to prevent distracters being added to personalization * or user history dictionaries */ public class DistracterFilter { private final Suggest mSuggest; private final Keyboard mKeyboard; + // If the score of the top suggestion exceeds this value, the tested word (e.g., + // an OOV, a misspelling, or an in-vocabulary word) would be considered as a distracter to + // words in dictionary. The greater the threshold is, the less likely the tested word would + // become a distracter, which means the tested word will be more likely to be added to + // the dictionary. + private static final float DISTRACTER_WORD_SCORE_THRESHOLD = 2.0f; + /** * Create a DistracterFilter instance. * * @param suggest an instance of Suggest which will be used to obtain a list of suggestions - * for a potential distracter/misspelling + * for a potential distracter * @param keyboard the keyboard that is currently being used. This information is needed * when calling mSuggest.getSuggestedWords(...) to obtain a list of suggestions. */ @@ -40,9 +54,79 @@ public class DistracterFilter { mKeyboard = keyboard; } - public boolean isDistracterToWordsInDictionaries(final String prevWord, - final String targetWord) { - // TODO: to be implemented + public static DistracterFilter createDistracterFilter(final Suggest suggest, + final KeyboardSwitcher keyboardSwitcher) { + final MainKeyboardView mainKeyboardView = keyboardSwitcher.getMainKeyboardView(); + // TODO: Create Keyboard when mainKeyboardView is null. + // TODO: Figure out the most reasonable keyboard for the filter. Refer to the + // spellchecker's logic. + final Keyboard keyboard = (mainKeyboardView != null) ? + mainKeyboardView.getKeyboard() : null; + final DistracterFilter distracterFilter = new DistracterFilter(suggest, keyboard); + return distracterFilter; + } + + private static boolean suggestionExceedsDistracterThreshold( + final SuggestedWordInfo suggestion, final String consideredWord, + final float distracterThreshold) { + if (null != suggestion) { + final int suggestionScore = suggestion.mScore; + final float normalizedScore = BinaryDictionaryUtils.calcNormalizedScore( + consideredWord, suggestion.mWord, suggestionScore); + if (normalizedScore > distracterThreshold) { + return true; + } + } return false; } + + /** + * Determine whether a word is a distracter to words in dictionaries. + * + * @param prevWord the previous word, or null if none. + * @param testedWord the word that will be tested to see whether it is a distracter to words + * in dictionaries. + * @return true if testedWord is a distracter, otherwise false. + */ + public boolean isDistracterToWordsInDictionaries(final String prevWord, + final String testedWord) { + if (mSuggest == null) { + return false; + } + + final WordComposer composer = new WordComposer(); + final int[] codePoints = StringUtils.toCodePointArray(testedWord); + final int[] coordinates; + if (null == mKeyboard) { + coordinates = CoordinateUtils.newCoordinateArray(codePoints.length, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); + } else { + coordinates = mKeyboard.getCoordinates(codePoints); + } + composer.setComposingWord(codePoints, coordinates, prevWord); + + final int trailingSingleQuotesCount = composer.trailingSingleQuotesCount(); + final String consideredWord = trailingSingleQuotesCount > 0 ? testedWord.substring(0, + testedWord.length() - trailingSingleQuotesCount) : testedWord; + final AsyncResultHolder<Boolean> holder = new AsyncResultHolder<Boolean>(); + final OnGetSuggestedWordsCallback callback = new OnGetSuggestedWordsCallback() { + @Override + public void onGetSuggestedWords(final SuggestedWords suggestedWords) { + if (suggestedWords != null && suggestedWords.size() > 1) { + // The suggestedWordInfo at 0 is the typed word. The 1st suggestion from + // the decoder is at index 1. + final SuggestedWordInfo firstSuggestion = suggestedWords.getInfo(1); + final boolean hasStrongDistractor = suggestionExceedsDistracterThreshold( + firstSuggestion, consideredWord, DISTRACTER_WORD_SCORE_THRESHOLD); + holder.set(hasStrongDistractor); + } + } + }; + mSuggest.getSuggestedWords(composer, prevWord, mKeyboard.getProximityInfo(), + true /* blockOffensiveWords */, true /* isCorrectionEnbaled */, + null /* additionalFeaturesOptions */, 0 /* sessionId */, + SuggestedWords.NOT_A_SEQUENCE_NUMBER, callback); + + return holder.get(false /* defaultValue */, Constants.GET_SUGGESTED_WORDS_TIMEOUT); + } } diff --git a/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java b/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java index 55061f45f..74e7db901 100644 --- a/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java +++ b/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java @@ -129,6 +129,9 @@ public final class LanguageModelParam { if (locale == null) { return null; } + // TODO: Though targetWord is an IV (in-vocabulary) word, we should still apply + // distracterFilter in the following code. If targetWord is a distracter, + // it should be filtered out. if (dictionaryFacilitator.isValidWord(targetWord, false /* ignoreCase */)) { return createAndGetLanguageModelParamOfWord(prevWord, targetWord, timestamp, true /* isValidWord */, locale); diff --git a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java index b37779bdc..938d27122 100644 --- a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java @@ -324,4 +324,8 @@ public final class SubtypeLocaleUtils { 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); + } } |