diff options
Diffstat (limited to 'java/src')
10 files changed, 413 insertions, 101 deletions
diff --git a/java/src/com/android/inputmethod/compat/EditorInfoCompatUtils.java b/java/src/com/android/inputmethod/compat/EditorInfoCompatUtils.java index 7eefa221a..43714829a 100644 --- a/java/src/com/android/inputmethod/compat/EditorInfoCompatUtils.java +++ b/java/src/com/android/inputmethod/compat/EditorInfoCompatUtils.java @@ -19,6 +19,7 @@ package com.android.inputmethod.compat; import android.view.inputmethod.EditorInfo; import java.lang.reflect.Field; +import java.util.Locale; public final class EditorInfoCompatUtils { // Note that EditorInfo.IME_FLAG_FORCE_ASCII has been introduced @@ -27,6 +28,8 @@ public final class EditorInfoCompatUtils { EditorInfo.class, "IME_FLAG_FORCE_ASCII"); private static final Integer OBJ_IME_FLAG_FORCE_ASCII = (Integer) CompatUtils.getFieldValue( null /* receiver */, null /* defaultValue */, FIELD_IME_FLAG_FORCE_ASCII); + private static final Field FIELD_HINT_LOCALES = CompatUtils.getField( + EditorInfo.class, "hintLocales"); private EditorInfoCompatUtils() { // This utility class is not publicly instantiable. @@ -78,4 +81,15 @@ public final class EditorInfoCompatUtils { } return (action != null) ? flags + action : flags.toString(); } + + public static Locale getPrimaryHintLocale(final EditorInfo editorInfo) { + if (editorInfo == null) { + return null; + } + final Object localeList = CompatUtils.getFieldValue(editorInfo, null, FIELD_HINT_LOCALES); + if (localeList == null) { + return null; + } + return LocaleListCompatUtils.getPrimary(localeList); + } } diff --git a/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java b/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java index 58ad4bd4c..d123a1799 100644 --- a/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java +++ b/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java @@ -17,14 +17,17 @@ package com.android.inputmethod.compat; import android.os.Build; +import android.text.TextUtils; import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.RichInputMethodSubtype; import com.android.inputmethod.latin.common.Constants; +import com.android.inputmethod.latin.common.LocaleUtils; import java.lang.reflect.Constructor; import java.lang.reflect.Method; +import java.util.Locale; import javax.annotation.Nonnull; @@ -78,6 +81,21 @@ public final class InputMethodSubtypeCompatUtils { || subtype.containsExtraValueKey(Constants.Subtype.ExtraValue.ASCII_CAPABLE); } + // Note that InputMethodSubtype.getLanguageTag() is expected to be available in Android N+. + private static final Method GET_LANGUAGE_TAG = + CompatUtils.getMethod(InputMethodSubtype.class, "getLanguageTag"); + + public static Locale getLocaleObject(final InputMethodSubtype subtype) { + // Locale.forLanguageTag() is available only in Android L and later. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + final String languageTag = (String) CompatUtils.invoke(subtype, null, GET_LANGUAGE_TAG); + if (!TextUtils.isEmpty(languageTag)) { + return Locale.forLanguageTag(languageTag); + } + } + return LocaleUtils.constructLocaleFromString(subtype.getLocale()); + } + @UsedForTesting public static boolean isAsciiCapableWithAPI(final InputMethodSubtype subtype) { return (Boolean)CompatUtils.invoke(subtype, false, METHOD_isAsciiCapable); diff --git a/java/src/com/android/inputmethod/compat/LocaleListCompatUtils.java b/java/src/com/android/inputmethod/compat/LocaleListCompatUtils.java new file mode 100644 index 000000000..01030aedd --- /dev/null +++ b/java/src/com/android/inputmethod/compat/LocaleListCompatUtils.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.compat; + +import java.lang.reflect.Method; +import java.util.Locale; + +public final class LocaleListCompatUtils { + private static final Class CLASS_LocaleList = CompatUtils.getClass("android.util.LocaleList"); + private static final Method METHOD_getPrimary = + CompatUtils.getMethod(CLASS_LocaleList, "getPrimary"); + + private LocaleListCompatUtils() { + // This utility class is not publicly instantiable. + } + + public static Locale getPrimary(final Object localeList) { + return (Locale) CompatUtils.invoke(localeList, null, METHOD_getPrimary); + } +} diff --git a/java/src/com/android/inputmethod/compat/UserManagerCompatUtils.java b/java/src/com/android/inputmethod/compat/UserManagerCompatUtils.java new file mode 100644 index 000000000..5dee31629 --- /dev/null +++ b/java/src/com/android/inputmethod/compat/UserManagerCompatUtils.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.compat; + +import android.content.Context; +import android.os.Build; +import android.os.UserManager; +import android.support.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.reflect.Method; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * A temporary solution until {@code UserManagerCompat.isUserUnlocked()} in the support-v4 library + * becomes publicly available. + */ +public final class UserManagerCompatUtils { + private static final Method METHOD_isUserUnlocked; + + static { + // We do not try to search the method in Android M and prior. + if (BuildCompatUtils.EFFECTIVE_SDK_INT <= Build.VERSION_CODES.M) { + METHOD_isUserUnlocked = null; + } else { + METHOD_isUserUnlocked = CompatUtils.getMethod(UserManager.class, "isUserUnlocked"); + } + } + + private UserManagerCompatUtils() { + // This utility class is not publicly instantiable. + } + + public static final int LOCK_STATE_UNKNOWN = 0; + public static final int LOCK_STATE_UNLOCKED = 1; + public static final int LOCK_STATE_LOCKED = 2; + + @Retention(SOURCE) + @IntDef({LOCK_STATE_UNKNOWN, LOCK_STATE_UNLOCKED, LOCK_STATE_LOCKED}) + public @interface LockState {} + + /** + * Check if the calling user is running in an "unlocked" state. A user is unlocked only after + * they've entered their credentials (such as a lock pattern or PIN), and credential-encrypted + * private app data storage is available. + * @param context context from which {@link UserManager} should be obtained. + * @return One of {@link LockState}. + */ + @LockState + public static int getUserLockState(final Context context) { + if (METHOD_isUserUnlocked == null) { + return LOCK_STATE_UNKNOWN; + } + final UserManager userManager = context.getSystemService(UserManager.class); + if (userManager == null) { + return LOCK_STATE_UNKNOWN; + } + final Boolean result = + (Boolean) CompatUtils.invoke(userManager, null, METHOD_isUserUnlocked); + if (result == null) { + return LOCK_STATE_UNKNOWN; + } + return result ? LOCK_STATE_UNLOCKED : LOCK_STATE_LOCKED; + } +} diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java index 47013fe9e..26ff051bb 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java @@ -32,6 +32,7 @@ import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.compat.EditorInfoCompatUtils; import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils; +import com.android.inputmethod.compat.UserManagerCompatUtils; import com.android.inputmethod.keyboard.internal.KeyboardBuilder; import com.android.inputmethod.keyboard.internal.KeyboardParams; import com.android.inputmethod.keyboard.internal.UniqueKeysCache; @@ -275,6 +276,16 @@ public final class KeyboardLayoutSet { params.mIsPasswordField = InputTypeUtils.isPasswordInputType(editorInfo.inputType); params.mNoSettingsKey = InputAttributes.inPrivateImeOptions( mPackageName, NO_SETTINGS_KEY, editorInfo); + + // When the device is still unlocked, features like showing the IME setting app need to + // be locked down. + // TODO: Switch to {@code UserManagerCompat.isUserUnlocked()} in the support-v4 library + // when it becomes publicly available. + @UserManagerCompatUtils.LockState + final int lockState = UserManagerCompatUtils.getUserLockState(context); + if (lockState == UserManagerCompatUtils.LOCK_STATE_LOCKED) { + params.mNoSettingsKey = true; + } } public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) { diff --git a/java/src/com/android/inputmethod/keyboard/emoji/EmojiCategory.java b/java/src/com/android/inputmethod/keyboard/emoji/EmojiCategory.java index 75b7962cb..b57e483d1 100644 --- a/java/src/com/android/inputmethod/keyboard/emoji/EmojiCategory.java +++ b/java/src/com/android/inputmethod/keyboard/emoji/EmojiCategory.java @@ -196,6 +196,8 @@ final class EmojiCategory { addShownCategoryId(EmojiCategory.ID_FLAGS); } } + } else { + addShownCategoryId(EmojiCategory.ID_SYMBOLS); } addShownCategoryId(EmojiCategory.ID_EMOTICONS); @@ -204,9 +206,14 @@ final class EmojiCategory { recentsKbd.loadRecentKeys(mCategoryKeyboardMap.values()); mCurrentCategoryId = Settings.readLastShownEmojiCategoryId(mPrefs, defaultCategoryId); - if (mCurrentCategoryId == EmojiCategory.ID_RECENTS && + Log.i(TAG, "Last Emoji category id is " + mCurrentCategoryId); + if (!isShownCategoryId(mCurrentCategoryId)) { + Log.i(TAG, "Last emoji category " + mCurrentCategoryId + + " is invalid, starting in " + defaultCategoryId); + mCurrentCategoryId = defaultCategoryId; + } else if (mCurrentCategoryId == EmojiCategory.ID_RECENTS && recentsKbd.getSortedKeys().isEmpty()) { - Log.i(TAG, "No recent emojis found, starting in category " + mCurrentCategoryId); + Log.i(TAG, "No recent emojis found, starting in category " + defaultCategoryId); mCurrentCategoryId = defaultCategoryId; } } @@ -219,6 +226,15 @@ final class EmojiCategory { mShownCategories.add(properties); } + private boolean isShownCategoryId(final int categoryId) { + for (final CategoryProperties prop : mShownCategories) { + if (prop.mCategoryId == categoryId) { + return true; + } + } + return false; + } + public static String getCategoryName(final int categoryId, final int categoryPageId) { return sCategoryName[categoryId] + "-" + categoryPageId; } diff --git a/java/src/com/android/inputmethod/latin/EmojiAltPhysicalKeyDetector.java b/java/src/com/android/inputmethod/latin/EmojiAltPhysicalKeyDetector.java index 2529424c0..8924e0a3d 100644 --- a/java/src/com/android/inputmethod/latin/EmojiAltPhysicalKeyDetector.java +++ b/java/src/com/android/inputmethod/latin/EmojiAltPhysicalKeyDetector.java @@ -18,14 +18,15 @@ package com.android.inputmethod.latin; import android.content.res.Resources; import android.util.Log; +import android.util.Pair; import android.view.KeyEvent; import com.android.inputmethod.keyboard.KeyboardSwitcher; import com.android.inputmethod.latin.settings.Settings; -import java.util.HashMap; +import java.util.ArrayList; import java.util.HashSet; -import java.util.Map; +import java.util.List; import java.util.Set; import javax.annotation.Nonnull; @@ -35,121 +36,172 @@ import javax.annotation.Nonnull; */ final class EmojiAltPhysicalKeyDetector { private static final String TAG = "EmojiAltPhysicalKeyDetector"; + private static final boolean DEBUG = false; - private final Map<Integer, Integer> mEmojiSwitcherMap; - private final Map<Integer, Integer> mSymbolsShiftedSwitcherMap; - private final Map<Integer, Integer> mCombinedSwitcherMap; + private List<EmojiHotKeys> mHotKeysList; - // Set of keys codes that have been used as modifiers. - private Set<Integer> mActiveModifiers; + private static class HotKeySet extends HashSet<Pair<Integer, Integer>> { }; + + private abstract class EmojiHotKeys { + private final String mName; + private final HotKeySet mKeySet; + + boolean mCanFire; + int mMetaState; + + public EmojiHotKeys(final String name, HotKeySet keySet) { + mName = name; + mKeySet = keySet; + mCanFire = false; + } + + public void onKeyDown(@Nonnull final KeyEvent keyEvent) { + if (DEBUG) { + Log.d(TAG, "EmojiHotKeys.onKeyDown() - " + mName + " - considering " + keyEvent); + } + + final Pair<Integer, Integer> key = + Pair.create(keyEvent.getKeyCode(), keyEvent.getMetaState()); + if (mKeySet.contains(key)) { + if (DEBUG) { + Log.d(TAG, "EmojiHotKeys.onKeyDown() - " + mName + " - enabling action"); + } + mCanFire = true; + mMetaState = keyEvent.getMetaState(); + } else if (mCanFire) { + if (DEBUG) { + Log.d(TAG, "EmojiHotKeys.onKeyDown() - " + mName + " - disabling action"); + } + mCanFire = false; + } + } + + public void onKeyUp(@Nonnull final KeyEvent keyEvent) { + if (DEBUG) { + Log.d(TAG, "EmojiHotKeys.onKeyUp() - " + mName + " - considering " + keyEvent); + } + + final int keyCode = keyEvent.getKeyCode(); + int metaState = keyEvent.getMetaState(); + if (KeyEvent.isModifierKey(keyCode)) { + // Try restoring meta stat in case the released key was a modifier. + // I am sure one can come up with scenarios to break this, but it + // seems to work well in practice. + metaState |= mMetaState; + } + + final Pair<Integer, Integer> key = Pair.create(keyCode, metaState); + if (mKeySet.contains(key)) { + if (mCanFire) { + if (!keyEvent.isCanceled()) { + if (DEBUG) { + Log.d(TAG, "EmojiHotKeys.onKeyUp() - " + mName + " - firing action"); + } + action(); + } else { + // This key up event was a part of key combinations and + // should be ignored. + if (DEBUG) { + Log.d(TAG, "EmojiHotKeys.onKeyUp() - " + mName + " - canceled, ignoring action"); + } + } + mCanFire = false; + } + } + + if (mCanFire) { + if (DEBUG) { + Log.d(TAG, "EmojiHotKeys.onKeyUp() - " + mName + " - disabling action"); + } + mCanFire = false; + } + } + + protected abstract void action(); + } public EmojiAltPhysicalKeyDetector(@Nonnull final Resources resources) { - mEmojiSwitcherMap = parseSwitchDefinition(resources, R.array.keyboard_switcher_emoji); - mSymbolsShiftedSwitcherMap = parseSwitchDefinition( + mHotKeysList = new ArrayList<EmojiHotKeys>(); + + final HotKeySet emojiSwitchSet = parseHotKeys( + resources, R.array.keyboard_switcher_emoji); + final EmojiHotKeys emojiHotKeys = new EmojiHotKeys("emoji", emojiSwitchSet) { + @Override + protected void action() { + final KeyboardSwitcher switcher = KeyboardSwitcher.getInstance(); + switcher.onToggleKeyboard(KeyboardSwitcher.KeyboardSwitchState.EMOJI); + } + }; + mHotKeysList.add(emojiHotKeys); + + final HotKeySet symbolsSwitchSet = parseHotKeys( resources, R.array.keyboard_switcher_symbols_shifted); - mCombinedSwitcherMap = new HashMap<>(); - mCombinedSwitcherMap.putAll(mEmojiSwitcherMap); - mCombinedSwitcherMap.putAll(mSymbolsShiftedSwitcherMap); - mActiveModifiers = new HashSet<>(); + final EmojiHotKeys symbolsHotKeys = new EmojiHotKeys("symbols", symbolsSwitchSet) { + @Override + protected void action() { + final KeyboardSwitcher switcher = KeyboardSwitcher.getInstance(); + switcher.onToggleKeyboard(KeyboardSwitcher.KeyboardSwitchState.SYMBOLS_SHIFTED); + } + }; + mHotKeysList.add(symbolsHotKeys); } - private static Map<Integer, Integer> parseSwitchDefinition( - @Nonnull final Resources resources, - final int resourceId) { - final Map<Integer, Integer> definition = new HashMap<>(); - final String name = resources.getResourceEntryName(resourceId); - final String[] values = resources.getStringArray(resourceId); - for (int i = 0; values != null && i < values.length; i++) { - String[] valuePair = values[i].split(","); - if (valuePair.length != 2) { - Log.w(TAG, "Expected 2 integers in " + name + "[" + i + "] : " + values[i]); - } - try { - definition.put(Integer.parseInt(valuePair[0]), Integer.parseInt(valuePair[1])); - } catch (NumberFormatException e) { - Log.w(TAG, "Failed to parse " + name + "[" + i + "] : " + values[i], e); + public void onKeyDown(@Nonnull final KeyEvent keyEvent) { + if (DEBUG) { + Log.d(TAG, "onKeyDown(): " + keyEvent); + } + + if (shouldProcessEvent(keyEvent)) { + for (EmojiHotKeys hotKeys : mHotKeysList) { + hotKeys.onKeyDown(keyEvent); } } - return definition; } - /** - * Determine whether an up key event came from a mapped modifier key. - * - * @param keyEvent an up key event. - */ public void onKeyUp(@Nonnull final KeyEvent keyEvent) { - Log.d(TAG, "onKeyUp() : " + keyEvent); - if (!Settings.getInstance().getCurrent().mEnableEmojiAltPhysicalKey) { - // The feature is disabled. - Log.d(TAG, "onKeyUp() : Disabled"); - return; + if (DEBUG) { + Log.d(TAG, "onKeyUp(): " + keyEvent); } - if (keyEvent.isCanceled()) { - // This key up event was a part of key combinations and should be ignored. - Log.d(TAG, "onKeyUp() : Canceled"); - return; - } - final Integer mappedModifier = getMappedModifier(keyEvent); - if (mappedModifier != null) { - // If the key was modified by a mapped key, then ignore the next time - // the same modifier key comes up. - Log.d(TAG, "onKeyUp() : Using Modifier: " + mappedModifier); - mActiveModifiers.add(mappedModifier); - return; - } - final int keyCode = keyEvent.getKeyCode(); - if (mActiveModifiers.contains(keyCode)) { - // Used as a modifier, not a standalone key press. - Log.d(TAG, "onKeyUp() : Used as Modifier: " + keyCode); - mActiveModifiers.remove(keyCode); - return; - } - if (!isMappedKeyCode(keyEvent)) { - // Nothing special about this key. - Log.d(TAG, "onKeyUp() : Not Mapped: " + keyCode); - return; - } - final KeyboardSwitcher switcher = KeyboardSwitcher.getInstance(); - if (mEmojiSwitcherMap.keySet().contains(keyCode)) { - switcher.onToggleKeyboard(KeyboardSwitcher.KeyboardSwitchState.EMOJI); - } else if (mSymbolsShiftedSwitcherMap.keySet().contains(keyCode)) { - switcher.onToggleKeyboard(KeyboardSwitcher.KeyboardSwitchState.SYMBOLS_SHIFTED); - } else { - Log.w(TAG, "Cannot toggle on keyCode: " + keyCode); + + if (shouldProcessEvent(keyEvent)) { + for (EmojiHotKeys hotKeys : mHotKeysList) { + hotKeys.onKeyUp(keyEvent); + } } } - /** - * @param keyEvent pressed key event - * @return true iff the user pressed a mapped modifier key. - */ - private boolean isMappedKeyCode(@Nonnull final KeyEvent keyEvent) { - return mCombinedSwitcherMap.get(keyEvent.getKeyCode()) != null; + private static boolean shouldProcessEvent(@Nonnull final KeyEvent keyEvent) { + if (!Settings.getInstance().getCurrent().mEnableEmojiAltPhysicalKey) { + // The feature is disabled. + if (DEBUG) { + Log.d(TAG, "shouldProcessEvent(): Disabled"); + } + return false; + } + + return true; } - /** - * @param keyEvent pressed key event - * @return the mapped modifier used with this key opress, if any. - */ - private Integer getMappedModifier(@Nonnull final KeyEvent keyEvent) { - final int keyCode = keyEvent.getKeyCode(); - final int metaState = keyEvent.getMetaState(); - for (int mappedKeyCode : mCombinedSwitcherMap.keySet()) { - if (keyCode == mappedKeyCode) { - Log.d(TAG, "getMappedModifier() : KeyCode = MappedKeyCode = " + mappedKeyCode); - continue; - } - final Integer mappedMeta = mCombinedSwitcherMap.get(mappedKeyCode); - if (mappedMeta == null || mappedMeta.intValue() == -1) { - continue; + private static HotKeySet parseHotKeys( + @Nonnull final Resources resources, final int resourceId) { + final HotKeySet keySet = new HotKeySet(); + final String name = resources.getResourceEntryName(resourceId); + final String[] values = resources.getStringArray(resourceId); + for (int i = 0; values != null && i < values.length; i++) { + String[] valuePair = values[i].split(","); + if (valuePair.length != 2) { + Log.w(TAG, "Expected 2 integers in " + name + "[" + i + "] : " + values[i]); } - if ((metaState & mappedMeta) != 0) { - Log.d(TAG, "getMappedModifier() : MetaState(" + metaState - + ") contains MappedMeta(" + mappedMeta + ")"); - return mappedKeyCode; + try { + final Integer keyCode = Integer.parseInt(valuePair[0]); + final Integer metaState = Integer.parseInt(valuePair[1]); + final Pair<Integer, Integer> key = Pair.create( + keyCode, KeyEvent.normalizeMetaState(metaState)); + keySet.add(key); + } catch (NumberFormatException e) { + Log.w(TAG, "Failed to parse " + name + "[" + i + "] : " + values[i], e); } } - return null; + return keySet; } } diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 25a5de250..c2d9f965f 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -30,6 +30,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Color; import android.inputmethodservice.InputMethodService; import android.media.AudioManager; import android.os.Debug; @@ -53,7 +54,9 @@ import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.accessibility.AccessibilityUtils; import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.compat.EditorInfoCompatUtils; import com.android.inputmethod.compat.InputMethodServiceCompatUtils; +import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils; import com.android.inputmethod.compat.ViewOutlineProviderCompatUtils; import com.android.inputmethod.compat.ViewOutlineProviderCompatUtils.InsetsUpdater; import com.android.inputmethod.dictionarypack.DictionaryPackConstants; @@ -175,8 +178,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private static final int MSG_WAIT_FOR_DICTIONARY_LOAD = 8; private static final int MSG_DEALLOCATE_MEMORY = 9; private static final int MSG_RESUME_SUGGESTIONS_FOR_START_INPUT = 10; + private static final int MSG_SWITCH_LANGUAGE_AUTOMATICALLY = 11; // Update this when adding new messages - private static final int MSG_LAST = MSG_RESUME_SUGGESTIONS_FOR_START_INPUT; + private static final int MSG_LAST = MSG_SWITCH_LANGUAGE_AUTOMATICALLY; private static final int ARG1_NOT_GESTURE_INPUT = 0; private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1; @@ -270,6 +274,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen case MSG_DEALLOCATE_MEMORY: latinIme.deallocateMemory(); break; + case MSG_SWITCH_LANGUAGE_AUTOMATICALLY: + latinIme.switchLanguage((InputMethodSubtype)msg.obj); + break; } } @@ -388,6 +395,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen obtainMessage(MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED, suggestedWords).sendToTarget(); } + public void postSwitchLanguage(final InputMethodSubtype subtype) { + obtainMessage(MSG_SWITCH_LANGUAGE_AUTOMATICALLY, subtype).sendToTarget(); + } + // Working variables for the following methods. private boolean mIsOrientationChanging; private boolean mPendingSuccessiveImsCallback; @@ -794,6 +805,19 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) { super.onStartInput(editorInfo, restarting); + + // If the primary hint language does not match the current subtype language, then try + // to switch to the primary hint language. + // TODO: Support all the locales in EditorInfo#hintLocales. + final Locale primaryHintLocale = EditorInfoCompatUtils.getPrimaryHintLocale(editorInfo); + if (primaryHintLocale == null) { + return; + } + final InputMethodSubtype newSubtype = mRichImm.findSubtypeByLocale(primaryHintLocale); + if (newSubtype == null || newSubtype.equals(mRichImm.getCurrentSubtype().getRawSubtype())) { + return; + } + mHandler.postSwitchLanguage(newSubtype); } @SuppressWarnings("deprecation") @@ -968,12 +992,19 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } @Override + public void onWindowShown() { + super.onWindowShown(); + setNavigationBarVisibility(isInputViewShown()); + } + + @Override public void onWindowHidden() { super.onWindowHidden(); final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView != null) { mainKeyboardView.closing(); } + setNavigationBarVisibility(false); } void onFinishInputInternal() { @@ -1293,6 +1324,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return mOptionsDialog != null && mOptionsDialog.isShowing(); } + public void switchLanguage(final InputMethodSubtype subtype) { + final IBinder token = getWindow().getWindow().getAttributes().token; + mRichImm.setInputMethodAndSubtype(token, subtype); + } + // TODO: Revise the language switch key behavior to make it much smarter and more reasonable. public void switchToNextSubtype() { final IBinder token = getWindow().getWindow().getAttributes().token; @@ -1650,6 +1686,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Hooks for hardware keyboard @Override public boolean onKeyDown(final int keyCode, final KeyEvent keyEvent) { + if (mEmojiAltPhysicalKeyDetector == null) { + mEmojiAltPhysicalKeyDetector = new EmojiAltPhysicalKeyDetector( + getApplicationContext().getResources()); + } + mEmojiAltPhysicalKeyDetector.onKeyDown(keyEvent); if (!ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED) { return super.onKeyDown(keyCode, keyEvent); } @@ -1865,4 +1906,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } return mRichImm.shouldOfferSwitchingToNextInputMethod(token, fallbackValue); } + + private void setNavigationBarVisibility(final boolean visible) { + // Color.BLACK is ignored and default IME navigation bar color is used. + getWindow().getWindow().setNavigationBarColor(visible ? Color.BLACK : Color.TRANSPARENT); + } } diff --git a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java index ef946c8bc..3beb51d68 100644 --- a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java +++ b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java @@ -32,6 +32,7 @@ import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.compat.InputMethodManagerCompatWrapper; +import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils; import com.android.inputmethod.latin.settings.Settings; import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils; import com.android.inputmethod.latin.utils.LanguageOnSpacebarUtils; @@ -428,6 +429,46 @@ public class RichInputMethodManager { return null; } + public InputMethodSubtype findSubtypeByLocale(final Locale locale) { + // Find the best subtype based on a straightforward matching algorithm. + // TODO: Use LocaleList#getFirstMatch() instead. + final List<InputMethodSubtype> subtypes = + getMyEnabledInputMethodSubtypeList(true /* allowsImplicitlySelectedSubtypes */); + final int count = subtypes.size(); + for (int i = 0; i < count; ++i) { + final InputMethodSubtype subtype = subtypes.get(i); + final Locale subtypeLocale = InputMethodSubtypeCompatUtils.getLocaleObject(subtype); + if (subtypeLocale.equals(locale)) { + return subtype; + } + } + for (int i = 0; i < count; ++i) { + final InputMethodSubtype subtype = subtypes.get(i); + final Locale subtypeLocale = InputMethodSubtypeCompatUtils.getLocaleObject(subtype); + if (subtypeLocale.getLanguage().equals(locale.getLanguage()) && + subtypeLocale.getCountry().equals(locale.getCountry()) && + subtypeLocale.getVariant().equals(locale.getVariant())) { + return subtype; + } + } + for (int i = 0; i < count; ++i) { + final InputMethodSubtype subtype = subtypes.get(i); + final Locale subtypeLocale = InputMethodSubtypeCompatUtils.getLocaleObject(subtype); + if (subtypeLocale.getLanguage().equals(locale.getLanguage()) && + subtypeLocale.getCountry().equals(locale.getCountry())) { + return subtype; + } + } + for (int i = 0; i < count; ++i) { + final InputMethodSubtype subtype = subtypes.get(i); + final Locale subtypeLocale = InputMethodSubtypeCompatUtils.getLocaleObject(subtype); + if (subtypeLocale.getLanguage().equals(locale.getLanguage())) { + return subtype; + } + } + return null; + } + public void setInputMethodAndSubtype(final IBinder token, final InputMethodSubtype subtype) { mImmWrapper.mImm.setInputMethodAndSubtype( token, getInputMethodIdOfThisIme(), subtype); diff --git a/java/src/com/android/inputmethod/latin/RichInputMethodSubtype.java b/java/src/com/android/inputmethod/latin/RichInputMethodSubtype.java index 9d7849ffc..71aaf5e01 100644 --- a/java/src/com/android/inputmethod/latin/RichInputMethodSubtype.java +++ b/java/src/com/android/inputmethod/latin/RichInputMethodSubtype.java @@ -47,7 +47,7 @@ public class RichInputMethodSubtype { public RichInputMethodSubtype(@Nonnull final InputMethodSubtype subtype) { mSubtype = subtype; - mLocale = LocaleUtils.constructLocaleFromString(mSubtype.getLocale()); + mLocale = InputMethodSubtypeCompatUtils.getLocaleObject(mSubtype); } // Extra values are determined by the primary subtype. This is probably right, but |