From 09eecc0502799badecdb64e7d30a1baa966075d8 Mon Sep 17 00:00:00 2001 From: Helium314 Date: Sat, 31 May 2025 13:22:22 +0200 Subject: [PATCH] move RichInputMethodManager to Kotlin --- .../latin/RichInputMethodManager.java | 445 ------------------ .../keyboard/latin/RichInputMethodManager.kt | 329 +++++++++++++ 2 files changed, 329 insertions(+), 445 deletions(-) delete mode 100644 app/src/main/java/helium314/keyboard/latin/RichInputMethodManager.java create mode 100644 app/src/main/java/helium314/keyboard/latin/RichInputMethodManager.kt diff --git a/app/src/main/java/helium314/keyboard/latin/RichInputMethodManager.java b/app/src/main/java/helium314/keyboard/latin/RichInputMethodManager.java deleted file mode 100644 index d5e19d334..000000000 --- a/app/src/main/java/helium314/keyboard/latin/RichInputMethodManager.java +++ /dev/null @@ -1,445 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * modified - * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only - */ - -package helium314.keyboard.latin; - -import android.content.Context; -import android.content.SharedPreferences; -import android.inputmethodservice.InputMethodService; -import android.os.AsyncTask; -import android.os.IBinder; -import android.view.inputmethod.InputMethodInfo; -import android.view.inputmethod.InputMethodManager; -import android.view.inputmethod.InputMethodSubtype; - -import helium314.keyboard.compat.ConfigurationCompatKt; -import helium314.keyboard.latin.common.LocaleUtils; -import helium314.keyboard.latin.settings.Settings; -import helium314.keyboard.latin.utils.KtxKt; -import helium314.keyboard.latin.utils.LanguageOnSpacebarUtils; -import helium314.keyboard.latin.utils.Log; -import helium314.keyboard.latin.utils.ScriptUtils; -import helium314.keyboard.latin.utils.SubtypeLocaleUtils; -import helium314.keyboard.latin.utils.SubtypeSettings; -import helium314.keyboard.latin.utils.SubtypeUtilsKt; -import kotlin.collections.CollectionsKt; - -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -import static helium314.keyboard.latin.common.Constants.Subtype.KEYBOARD_MODE; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -/** - * Enrichment class for InputMethodManager to simplify interaction and add functionality. - */ -// non final for easy mocking. -public class RichInputMethodManager { - private static final String TAG = RichInputMethodManager.class.getSimpleName(); - private static final boolean DEBUG = false; - - private RichInputMethodManager() { - // This utility class is not publicly instantiable. - } - - private static final RichInputMethodManager sInstance = new RichInputMethodManager(); - - private Context mContext; - private InputMethodManager mImm; - private InputMethodInfoCache mInputMethodInfoCache; - private RichInputMethodSubtype mCurrentRichInputMethodSubtype; - private InputMethodInfo mShortcutInputMethodInfo; - private InputMethodSubtype mShortcutSubtype; - - private static final int INDEX_NOT_FOUND = -1; - - public static RichInputMethodManager getInstance() { - sInstance.checkInitialized(); - return sInstance; - } - - public static void init(final Context context) { - sInstance.initInternal(context); - } - - private boolean isInitializedInternal() { - return mImm != null; - } - - public static boolean isInitialized() { - return sInstance.isInitializedInternal(); - } - - private void checkInitialized() { - if (!isInitializedInternal()) { - throw new RuntimeException(TAG + " is used before initialization"); - } - } - - private void initInternal(final Context context) { - if (isInitializedInternal()) { - return; - } - mImm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); - mContext = context; - mInputMethodInfoCache = new InputMethodInfoCache(mImm, context.getPackageName()); - - // Initialize the current input method subtype and the shortcut IME. - refreshSubtypeCaches(); - } - - public InputMethodManager getInputMethodManager() { - checkInitialized(); - return mImm; - } - - public List getMyEnabledInputMethodSubtypeList( - boolean allowsImplicitlySelectedSubtypes) { - return getEnabledInputMethodSubtypeList( - getInputMethodInfoOfThisIme(), allowsImplicitlySelectedSubtypes); - } - - public @Nullable InputMethodSubtype getNextSubtypeInThisIme(final boolean onlyCurrentIme) { - final InputMethodSubtype currentSubtype = getCurrentSubtype().getRawSubtype(); - final List enabledSubtypes = getMyEnabledInputMethodSubtypeList(true); - final int currentIndex = enabledSubtypes.indexOf(currentSubtype); - if (currentIndex == INDEX_NOT_FOUND) { - Log.w(TAG, "Can't find current subtype in enabled subtypes: subtype=" - + SubtypeLocaleUtils.getSubtypeNameForLogging(currentSubtype)); - if (onlyCurrentIme) return enabledSubtypes.get(0); // just return first enabled subtype - else return null; - } - final int nextIndex = (currentIndex + 1) % enabledSubtypes.size(); - if (nextIndex <= currentIndex && !onlyCurrentIme) { - // The current subtype is the last or only enabled one and it needs to switch to next IME. - return null; - } - return enabledSubtypes.get(nextIndex); - } - - private static class InputMethodInfoCache { - private final InputMethodManager mImm; - private final String mImePackageName; - - private InputMethodInfo mCachedThisImeInfo; - private final HashMap> - mCachedSubtypeListWithImplicitlySelected; - private final HashMap> - mCachedSubtypeListOnlyExplicitlySelected; - - public InputMethodInfoCache(final InputMethodManager imm, final String imePackageName) { - mImm = imm; - mImePackageName = imePackageName; - mCachedSubtypeListWithImplicitlySelected = new HashMap<>(); - mCachedSubtypeListOnlyExplicitlySelected = new HashMap<>(); - } - - public synchronized InputMethodInfo getInputMethodOfThisIme() { - if (mCachedThisImeInfo != null) { - return mCachedThisImeInfo; - } - final var inputMethods = mImm.getInputMethodList(); - for (final InputMethodInfo imi : inputMethods) { - if (imi.getPackageName().equals(mImePackageName)) { - mCachedThisImeInfo = imi; - return imi; - } - } - throw new RuntimeException("Input method id for " + mImePackageName + " not found, only found" + - CollectionsKt.map(inputMethods, InputMethodInfo::getPackageName)); - } - - public synchronized List getEnabledInputMethodSubtypeList( - final InputMethodInfo imi, final boolean allowsImplicitlySelectedSubtypes) { - final HashMap> cache = - allowsImplicitlySelectedSubtypes - ? mCachedSubtypeListWithImplicitlySelected - : mCachedSubtypeListOnlyExplicitlySelected; - final List cachedList = cache.get(imi); - if (cachedList != null) { - return cachedList; - } - final List result; - if (imi == getInputMethodOfThisIme()) { - // allowsImplicitlySelectedSubtypes means system should choose if nothing is enabled, - // use it to fall back to system locales or en_US to avoid returning an empty list - result = SubtypeSettings.INSTANCE.getEnabledSubtypes(allowsImplicitlySelectedSubtypes); - } else { - result = mImm.getEnabledInputMethodSubtypeList(imi, allowsImplicitlySelectedSubtypes); - } - cache.put(imi, result); - return result; - } - - public synchronized void clear() { - mCachedThisImeInfo = null; - mCachedSubtypeListWithImplicitlySelected.clear(); - mCachedSubtypeListOnlyExplicitlySelected.clear(); - } - } - - public InputMethodInfo getInputMethodInfoOfThisIme() { - return mInputMethodInfoCache.getInputMethodOfThisIme(); - } - - public boolean checkIfSubtypeBelongsToThisImeAndEnabled(final InputMethodSubtype subtype) { - return getEnabledInputMethodSubtypeList(getInputMethodInfoOfThisIme(), true) - .contains(subtype); - } - - public boolean checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(final InputMethodSubtype subtype) { - final boolean subtypeEnabled = checkIfSubtypeBelongsToThisImeAndEnabled(subtype); - final boolean subtypeExplicitlyEnabled = getMyEnabledInputMethodSubtypeList(false) - .contains(subtype); - return subtypeEnabled && !subtypeExplicitlyEnabled; - } - - public void onSubtypeChanged(@NonNull final InputMethodSubtype newSubtype) { - updateCurrentSubtype(newSubtype); - updateShortcutIme(); - if (DEBUG) { - Log.w(TAG, "onSubtypeChanged: " + mCurrentRichInputMethodSubtype); - } - } - - private static RichInputMethodSubtype sForcedSubtypeForTesting = null; - - static void forceSubtype(@NonNull final InputMethodSubtype subtype) { - sForcedSubtypeForTesting = RichInputMethodSubtype.Companion.get(subtype); - } - - @NonNull - public Locale getCurrentSubtypeLocale() { - if (null != sForcedSubtypeForTesting) { - return sForcedSubtypeForTesting.getLocale(); - } - return getCurrentSubtype().getLocale(); - } - - @NonNull - public RichInputMethodSubtype getCurrentSubtype() { - if (null != sForcedSubtypeForTesting) { - return sForcedSubtypeForTesting; - } - return mCurrentRichInputMethodSubtype; - } - - - public String getCombiningRulesExtraValueOfCurrentSubtype() { - return SubtypeLocaleUtils.getCombiningRulesExtraValue(getCurrentSubtype().getRawSubtype()); - } - - public boolean hasMultipleEnabledIMEsOrSubtypes(final boolean shouldIncludeAuxiliarySubtypes) { - final List enabledImis = mImm.getEnabledInputMethodList(); - return hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, enabledImis); - } - - public boolean hasMultipleEnabledSubtypesInThisIme( - final boolean shouldIncludeAuxiliarySubtypes) { - final List imiList = Collections.singletonList( - getInputMethodInfoOfThisIme()); - return hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, imiList); - } - - private boolean hasMultipleEnabledSubtypes(final boolean shouldIncludeAuxiliarySubtypes, - final List imiList) { - // Number of the filtered IMEs - int filteredImisCount = 0; - - for (InputMethodInfo imi : imiList) { - // We can return true immediately after we find two or more filtered IMEs. - if (filteredImisCount > 1) return true; - final List subtypes = getEnabledInputMethodSubtypeList(imi, true); - // IMEs that have no subtypes should be counted. - if (subtypes.isEmpty()) { - ++filteredImisCount; - continue; - } - - int auxCount = 0; - for (InputMethodSubtype subtype : subtypes) { - if (subtype.isAuxiliary()) { - ++auxCount; - } - } - final int nonAuxCount = subtypes.size() - auxCount; - - // IMEs that have one or more non-auxiliary subtypes should be counted. - // If shouldIncludeAuxiliarySubtypes is true, IMEs that have two or more auxiliary - // subtypes should be counted as well. - if (nonAuxCount > 0 || (shouldIncludeAuxiliarySubtypes && auxCount > 1)) { - ++filteredImisCount; - } - } - - if (filteredImisCount > 1) { - return true; - } - final List subtypes = getMyEnabledInputMethodSubtypeList(true); - int keyboardCount = 0; - // imm.getEnabledInputMethodSubtypeList(null, true) will return the current IME's - // both explicitly and implicitly enabled input method subtype. - // (The current IME should be LatinIME.) - for (InputMethodSubtype subtype : subtypes) { - if (KEYBOARD_MODE.equals(subtype.getMode())) { - ++keyboardCount; - } - } - return keyboardCount > 1; - } - - public InputMethodSubtype findSubtypeByLocaleAndKeyboardLayoutSet(final Locale locale, - final String keyboardLayoutSetName) { - final InputMethodInfo myImi = getInputMethodInfoOfThisIme(); - final int count = myImi.getSubtypeCount(); - for (int i = 0; i < count; i++) { - final InputMethodSubtype subtype = myImi.getSubtypeAt(i); - final String layoutName = SubtypeUtilsKt.mainLayoutNameOrQwerty(subtype); - if (locale.equals(SubtypeUtilsKt.locale(subtype)) && keyboardLayoutSetName.equals(layoutName)) { - return subtype; - } - } - return null; - } - - public InputMethodSubtype findSubtypeForHintLocale(final Locale locale) { - // Find the best subtype based on a locale matching - final List subtypes = getMyEnabledInputMethodSubtypeList(true); - InputMethodSubtype bestMatch = LocaleUtils.getBestMatch(locale, subtypes, SubtypeUtilsKt::locale); - if (bestMatch != null) return bestMatch; - - // search for first secondary language & script match - final int count = subtypes.size(); - final String language = locale.getLanguage(); - final String script = ScriptUtils.script(locale); - for (int i = 0; i < count; ++i) { - final InputMethodSubtype subtype = subtypes.get(i); - final Locale subtypeLocale = SubtypeUtilsKt.locale(subtype); - if (!ScriptUtils.script(subtypeLocale).equals(script)) - continue; // need compatible script - bestMatch = subtype; - final List secondaryLocales = SubtypeUtilsKt.getSecondaryLocales(subtype.getExtraValue()); - for (final Locale secondaryLocale : secondaryLocales) { - if (secondaryLocale.getLanguage().equals(language)) { - return bestMatch; - } - } - } - // if wanted script is not compatible to current subtype, return a subtype with compatible script if possible - if (!script.equals(ScriptUtils.script(getCurrentSubtypeLocale()))) { - return bestMatch; - } - return null; - } - - public List getEnabledInputMethodSubtypeList(final InputMethodInfo imi, - final boolean allowsImplicitlySelectedSubtypes) { - return mInputMethodInfoCache.getEnabledInputMethodSubtypeList( - imi, allowsImplicitlySelectedSubtypes); - } - - public void refreshSubtypeCaches() { - mInputMethodInfoCache.clear(); - SharedPreferences prefs = KtxKt.prefs(mContext); - updateCurrentSubtype(SubtypeSettings.INSTANCE.getSelectedSubtype(prefs)); - updateShortcutIme(); - } - - private void updateCurrentSubtype(final InputMethodSubtype subtype) { - SubtypeSettings.INSTANCE.setSelectedSubtype(KtxKt.prefs(mContext), subtype); - mCurrentRichInputMethodSubtype = RichInputMethodSubtype.Companion.get(subtype); - } - - public static boolean canSwitchLanguage() { - if (!isInitialized()) return false; - if (Settings.getValues().mLanguageSwitchKeyToOtherSubtypes && getInstance().hasMultipleEnabledSubtypesInThisIme(false)) - return true; - if (Settings.getValues().mLanguageSwitchKeyToOtherImes && getInstance().mImm.getEnabledInputMethodList().size() > 1) - return true; - return false; - } - - // todo: is shortcutIme only voice input, or can it be something else? - // if always voice input, rename it and other things like mHasShortcutKey - private void updateShortcutIme() { - if (DEBUG) { - Log.d(TAG, "Update shortcut IME from : " - + (mShortcutInputMethodInfo == null - ? "" : mShortcutInputMethodInfo.getId()) + ", " - + (mShortcutSubtype == null ? "" : ( - mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode()))); - } - final RichInputMethodSubtype richSubtype = mCurrentRichInputMethodSubtype; - final boolean implicitlyEnabledSubtype = checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled( - richSubtype.getRawSubtype()); - final Locale systemLocale = ConfigurationCompatKt.locale(mContext.getResources().getConfiguration()); - LanguageOnSpacebarUtils.onSubtypeChanged( - richSubtype, implicitlyEnabledSubtype, systemLocale); - LanguageOnSpacebarUtils.setEnabledSubtypes(getMyEnabledInputMethodSubtypeList( - true /* allowsImplicitlySelectedSubtypes */)); - - // TODO: Update an icon for shortcut IME - final Map> shortcuts = - getInputMethodManager().getShortcutInputMethodsAndSubtypes(); - mShortcutInputMethodInfo = null; - mShortcutSubtype = null; - for (final InputMethodInfo imi : shortcuts.keySet()) { - final List subtypes = shortcuts.get(imi); - // TODO: Returns the first found IMI for now. Should handle all shortcuts as - // appropriate. - mShortcutInputMethodInfo = imi; - // TODO: Pick up the first found subtype for now. Should handle all subtypes - // as appropriate. - mShortcutSubtype = subtypes.size() > 0 ? subtypes.get(0) : null; - break; - } - if (DEBUG) { - Log.d(TAG, "Update shortcut IME to : " - + (mShortcutInputMethodInfo == null - ? "" : mShortcutInputMethodInfo.getId()) + ", " - + (mShortcutSubtype == null ? "" : ( - mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode()))); - } - } - - public void switchToShortcutIme(final InputMethodService context) { - if (mShortcutInputMethodInfo == null) { - return; - } - - final String imiId = mShortcutInputMethodInfo.getId(); - switchToTargetIME(imiId, mShortcutSubtype, context); - } - - public boolean hasShortcutIme() { - return mShortcutInputMethodInfo != null; - } - - private void switchToTargetIME(final String imiId, final InputMethodSubtype subtype, - final InputMethodService context) { - final IBinder token = context.getWindow().getWindow().getAttributes().token; - if (token == null) { - return; - } - final InputMethodManager imm = getInputMethodManager(); - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - imm.setInputMethodAndSubtype(token, imiId, subtype); - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - public boolean isShortcutImeReady() { - return mShortcutInputMethodInfo != null; - } -} diff --git a/app/src/main/java/helium314/keyboard/latin/RichInputMethodManager.kt b/app/src/main/java/helium314/keyboard/latin/RichInputMethodManager.kt new file mode 100644 index 000000000..1bfb0bb71 --- /dev/null +++ b/app/src/main/java/helium314/keyboard/latin/RichInputMethodManager.kt @@ -0,0 +1,329 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * modified + * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only + */ +package helium314.keyboard.latin + +import android.content.Context +import android.inputmethodservice.InputMethodService +import android.os.AsyncTask +import android.view.inputmethod.InputMethodInfo +import android.view.inputmethod.InputMethodManager +import android.view.inputmethod.InputMethodSubtype +import helium314.keyboard.compat.locale +import helium314.keyboard.latin.common.Constants +import helium314.keyboard.latin.common.LocaleUtils.getBestMatch +import helium314.keyboard.latin.settings.Settings +import helium314.keyboard.latin.utils.LanguageOnSpacebarUtils +import helium314.keyboard.latin.utils.Log +import helium314.keyboard.latin.utils.ScriptUtils.script +import helium314.keyboard.latin.utils.SubtypeLocaleUtils +import helium314.keyboard.latin.utils.SubtypeSettings +import helium314.keyboard.latin.utils.getSecondaryLocales +import helium314.keyboard.latin.utils.locale +import helium314.keyboard.latin.utils.mainLayoutNameOrQwerty +import helium314.keyboard.latin.utils.prefs +import java.util.Locale + +/** Enrichment class for InputMethodManager to simplify interaction and add functionality. */ +class RichInputMethodManager private constructor() { + private lateinit var context: Context + private lateinit var imm: InputMethodManager + private lateinit var inputMethodInfoCache: InputMethodInfoCache + private lateinit var currentRichInputMethodSubtype: RichInputMethodSubtype + private var shortcutInputMethodInfo: InputMethodInfo? = null + private var shortcutSubtype: InputMethodSubtype? = null + + private val isInitializedInternal get() = this::imm.isInitialized + + val currentSubtypeLocale get() = forcedSubtypeForTesting?.locale ?: currentSubtype.locale + + val currentSubtype get() = forcedSubtypeForTesting ?: currentRichInputMethodSubtype + + val combiningRulesExtraValueOfCurrentSubtype get() = + SubtypeLocaleUtils.getCombiningRulesExtraValue(currentSubtype.rawSubtype) + + val inputMethodInfoOfThisIme get() = inputMethodInfoCache.inputMethodOfThisIme + + val inputMethodManager: InputMethodManager get() { + checkInitialized() + return imm + } + + val isShortcutImeReady get() = shortcutInputMethodInfo != null + + fun hasShortcutIme() = isShortcutImeReady // todo + + fun checkIfSubtypeBelongsToThisImeAndEnabled(subtype: InputMethodSubtype?) = + getEnabledInputMethodSubtypeList(inputMethodInfoOfThisIme, true).contains(subtype) + + // todo: same as SubtypeSettings.getEnabledSubtypes(allowsImplicitlySelectedSubtypes), right? + fun getMyEnabledInputMethodSubtypeList(allowsImplicitlySelectedSubtypes: Boolean) = + getEnabledInputMethodSubtypeList(inputMethodInfoOfThisIme, allowsImplicitlySelectedSubtypes) + + fun getEnabledInputMethodSubtypeList(imi: InputMethodInfo, allowsImplicitlySelectedSubtypes: Boolean) = + inputMethodInfoCache.getEnabledInputMethodSubtypeList(imi, allowsImplicitlySelectedSubtypes) + + // could also check SubtypeSettings.getEnabledSubtypes(allowsImplicitlySelectedSubtypes) + fun checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(subtype: InputMethodSubtype): Boolean { + val subtypeEnabled = checkIfSubtypeBelongsToThisImeAndEnabled(subtype) + val subtypeExplicitlyEnabled = getMyEnabledInputMethodSubtypeList(false) + .contains(subtype) + return subtypeEnabled && !subtypeExplicitlyEnabled + } + + fun hasMultipleEnabledIMEsOrSubtypes(shouldIncludeAuxiliarySubtypes: Boolean) = + hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, imm.enabledInputMethodList) + + fun hasMultipleEnabledSubtypesInThisIme(shouldIncludeAuxiliarySubtypes: Boolean) = + hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, listOf(inputMethodInfoOfThisIme)) + + fun getNextSubtypeInThisIme(onlyCurrentIme: Boolean): InputMethodSubtype? { + val currentSubtype = currentSubtype.rawSubtype + val enabledSubtypes = getMyEnabledInputMethodSubtypeList(true) + val currentIndex = enabledSubtypes.indexOf(currentSubtype) + if (currentIndex == -1) { + Log.w(TAG, "Can't find current subtype in enabled subtypes: subtype=" + + SubtypeLocaleUtils.getSubtypeNameForLogging(currentSubtype)) + return if (onlyCurrentIme) enabledSubtypes[0] // just return first enabled subtype + else null + } + val nextIndex = (currentIndex + 1) % enabledSubtypes.size + if (nextIndex <= currentIndex && !onlyCurrentIme) { + // The current subtype is the last or only enabled one and it needs to switch to next IME. + return null + } + return enabledSubtypes[nextIndex] + } + + // todo: this is about main layout, not layout set + fun findSubtypeByLocaleAndKeyboardLayoutSet(locale: Locale, keyboardLayoutSetName: String): InputMethodSubtype? { + val myImi = inputMethodInfoOfThisIme + val count = myImi.subtypeCount + for (i in 0..() { + override fun doInBackground(vararg params: Void?): Void? { + imm.setInputMethodAndSubtype(token, imiId, shortcutSubtype) + return null + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) + } + + // todo: is shortcutIme only voice input, or can it be something else? + // if always voice input, rename it and other things like mHasShortcutKey + private fun updateShortcutIme() { + if (DEBUG) { + val subtype = shortcutSubtype?.let { "${it.locale()}, ${it.mode}" } ?: "" + Log.d(TAG, ("Update shortcut IME from: ${shortcutInputMethodInfo?.id ?: ""}, $subtype")) + } + val richSubtype = currentRichInputMethodSubtype + val implicitlyEnabledSubtype = checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(richSubtype.rawSubtype) + val systemLocale = context.resources.configuration.locale() + LanguageOnSpacebarUtils.onSubtypeChanged(richSubtype, implicitlyEnabledSubtype, systemLocale) + LanguageOnSpacebarUtils.setEnabledSubtypes(getMyEnabledInputMethodSubtypeList(true)) + + // TODO: Update an icon for shortcut IME + val shortcuts = inputMethodManager.shortcutInputMethodsAndSubtypes + shortcutInputMethodInfo = null + shortcutSubtype = null + for (imi in shortcuts.keys) { + val subtypes = shortcuts[imi] ?: continue + // TODO: Returns the first found IMI for now. Should handle all shortcuts as appropriate. + shortcutInputMethodInfo = imi + // TODO: Pick up the first found subtype for now. Should handle all subtypes as appropriate. + shortcutSubtype = if (subtypes.size > 0) subtypes[0] else null + break + } + if (DEBUG) { + val subtype = shortcutSubtype?.let { "${it.locale()}, ${it.mode}" } ?: "" + Log.d(TAG, ("Update shortcut IME to: ${shortcutInputMethodInfo?.id ?: ""}, $subtype")) + } + } + + private fun hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes: Boolean, imiList: List): Boolean { + // Number of the filtered IMEs + var filteredImisCount = 0 + + imiList.forEach { imi -> + // We can return true immediately after we find two or more filtered IMEs. + if (filteredImisCount > 1) return true + val subtypes = getEnabledInputMethodSubtypeList(imi, true) + // IMEs that have no subtypes should be counted. + if (subtypes.isEmpty()) { + ++filteredImisCount + return@forEach + } + + var auxCount = 0 + for (subtype in subtypes) { + if (!subtype.isAuxiliary) { + // IMEs that have one or more non-auxiliary subtypes should be counted. + ++filteredImisCount + return@forEach + } + ++auxCount + } + + // If shouldIncludeAuxiliarySubtypes is true, IMEs that have two or more auxiliary + // subtypes should be counted as well. + if (shouldIncludeAuxiliarySubtypes && auxCount > 1) { + ++filteredImisCount + } + } + + if (filteredImisCount > 1) { + return true + } + val subtypes = getMyEnabledInputMethodSubtypeList(true) + // imm.getEnabledInputMethodSubtypeList(null, true) will return the current IME's + // both explicitly and implicitly enabled input method subtype. + // (The current IME should be LatinIME.) + return subtypes.count { it.mode == Constants.Subtype.KEYBOARD_MODE } > 1 + } + + private fun checkInitialized() { + if (!isInitializedInternal) { + throw RuntimeException("$TAG is used before initialization") + } + } + + private fun initInternal(ctx: Context) { + if (isInitializedInternal) { + return + } + imm = ctx.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + context = ctx + inputMethodInfoCache = InputMethodInfoCache(imm, ctx.packageName) + + // Initialize the current input method subtype and the shortcut IME. + refreshSubtypeCaches() + } + + companion object { + private val TAG = RichInputMethodManager::class.java.simpleName + private const val DEBUG = false + + private val instance = RichInputMethodManager() + + @JvmStatic + fun getInstance(): RichInputMethodManager { + instance.checkInitialized() + return instance + } + + fun init(ctx: Context) { + instance.initInternal(ctx) + } + + @JvmStatic + fun isInitialized() = instance.isInitializedInternal + + private var forcedSubtypeForTesting: RichInputMethodSubtype? = null + + fun forceSubtype(subtype: InputMethodSubtype) { + forcedSubtypeForTesting = RichInputMethodSubtype.get(subtype) + } + + fun canSwitchLanguage(): Boolean { + if (!isInitialized()) return false + if (Settings.getValues().mLanguageSwitchKeyToOtherSubtypes && instance.hasMultipleEnabledSubtypesInThisIme(false)) return true + if (Settings.getValues().mLanguageSwitchKeyToOtherImes && instance.imm.enabledInputMethodList.size > 1) return true + return false + } + } +} + +private class InputMethodInfoCache(private val imm: InputMethodManager, private val imePackageName: String) { + private var cachedThisImeInfo: InputMethodInfo? = null + private val cachedSubtypeListWithImplicitlySelected = HashMap>() + + private val cachedSubtypeListOnlyExplicitlySelected = HashMap>() + + @get:Synchronized + val inputMethodOfThisIme: InputMethodInfo get() { + if (cachedThisImeInfo == null) + cachedThisImeInfo = imm.inputMethodList.firstOrNull { it.packageName == imePackageName } + cachedThisImeInfo?.let { return it } + throw RuntimeException("Input method id for $imePackageName not found, only found " + + imm.inputMethodList.map { it.packageName }) + } + + @Synchronized + fun getEnabledInputMethodSubtypeList(imi: InputMethodInfo, allowsImplicitlySelectedSubtypes: Boolean): List { + val cache = if (allowsImplicitlySelectedSubtypes) cachedSubtypeListWithImplicitlySelected + else cachedSubtypeListOnlyExplicitlySelected + cache[imi]?.let { return it } + val result = if (imi === inputMethodOfThisIme) { + // allowsImplicitlySelectedSubtypes means system should choose if nothing is enabled, + // use it to fall back to system locales or en_US to avoid returning an empty list + SubtypeSettings.getEnabledSubtypes(allowsImplicitlySelectedSubtypes) + } else { + imm.getEnabledInputMethodSubtypeList(imi, allowsImplicitlySelectedSubtypes) + } + cache[imi] = result + return result + } + + @Synchronized + fun clear() { + cachedThisImeInfo = null + cachedSubtypeListWithImplicitlySelected.clear() + cachedSubtypeListOnlyExplicitlySelected.clear() + } +}