move RichInputMethodManager to Kotlin

This commit is contained in:
Helium314 2025-05-31 13:22:22 +02:00
parent 424df5fb0d
commit 09eecc0502
2 changed files with 329 additions and 445 deletions

View file

@ -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<InputMethodSubtype> getMyEnabledInputMethodSubtypeList(
boolean allowsImplicitlySelectedSubtypes) {
return getEnabledInputMethodSubtypeList(
getInputMethodInfoOfThisIme(), allowsImplicitlySelectedSubtypes);
}
public @Nullable InputMethodSubtype getNextSubtypeInThisIme(final boolean onlyCurrentIme) {
final InputMethodSubtype currentSubtype = getCurrentSubtype().getRawSubtype();
final List<InputMethodSubtype> 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<InputMethodInfo, List<InputMethodSubtype>>
mCachedSubtypeListWithImplicitlySelected;
private final HashMap<InputMethodInfo, List<InputMethodSubtype>>
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<InputMethodSubtype> getEnabledInputMethodSubtypeList(
final InputMethodInfo imi, final boolean allowsImplicitlySelectedSubtypes) {
final HashMap<InputMethodInfo, List<InputMethodSubtype>> cache =
allowsImplicitlySelectedSubtypes
? mCachedSubtypeListWithImplicitlySelected
: mCachedSubtypeListOnlyExplicitlySelected;
final List<InputMethodSubtype> cachedList = cache.get(imi);
if (cachedList != null) {
return cachedList;
}
final List<InputMethodSubtype> 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<InputMethodInfo> enabledImis = mImm.getEnabledInputMethodList();
return hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, enabledImis);
}
public boolean hasMultipleEnabledSubtypesInThisIme(
final boolean shouldIncludeAuxiliarySubtypes) {
final List<InputMethodInfo> imiList = Collections.singletonList(
getInputMethodInfoOfThisIme());
return hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, imiList);
}
private boolean hasMultipleEnabledSubtypes(final boolean shouldIncludeAuxiliarySubtypes,
final List<InputMethodInfo> 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<InputMethodSubtype> 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<InputMethodSubtype> 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<InputMethodSubtype> 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<Locale> 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<InputMethodSubtype> 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
? "<null>" : mShortcutInputMethodInfo.getId()) + ", "
+ (mShortcutSubtype == null ? "<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<InputMethodInfo, List<InputMethodSubtype>> shortcuts =
getInputMethodManager().getShortcutInputMethodsAndSubtypes();
mShortcutInputMethodInfo = null;
mShortcutSubtype = null;
for (final InputMethodInfo imi : shortcuts.keySet()) {
final List<InputMethodSubtype> subtypes = shortcuts.get(imi);
// TODO: Returns the first found IMI for now. Should handle all shortcuts as
// appropriate.
mShortcutInputMethodInfo = imi;
// 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
? "<null>" : mShortcutInputMethodInfo.getId()) + ", "
+ (mShortcutSubtype == null ? "<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<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
imm.setInputMethodAndSubtype(token, imiId, subtype);
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
public boolean isShortcutImeReady() {
return mShortcutInputMethodInfo != null;
}
}

View file

@ -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..<count) {
val subtype = myImi.getSubtypeAt(i)
if (locale == subtype.locale() && keyboardLayoutSetName == subtype.mainLayoutNameOrQwerty()) {
return subtype
}
}
return null
}
fun findSubtypeForHintLocale(locale: Locale): InputMethodSubtype? {
// Find the best subtype based on a locale matching
val subtypes = getMyEnabledInputMethodSubtypeList(true)
var bestMatch = getBestMatch(locale, subtypes) { it.locale() }
if (bestMatch != null) return bestMatch
// search for first secondary language & script match
val language = locale.language
val script = locale.script()
for (subtype in subtypes) {
val subtypeLocale = subtype.locale()
if (subtypeLocale.script() != script) continue // need compatible script
bestMatch = subtype
val secondaryLocales = getSecondaryLocales(subtype.extraValue)
for (secondaryLocale in secondaryLocales) {
if (secondaryLocale.language == language) {
return bestMatch
}
}
}
// if wanted script is not compatible to current subtype, return a subtype with compatible script if available
if (script != currentSubtypeLocale.script()) {
return bestMatch
}
return null
}
fun onSubtypeChanged(newSubtype: InputMethodSubtype) {
SubtypeSettings.setSelectedSubtype(context.prefs(), newSubtype)
currentRichInputMethodSubtype = RichInputMethodSubtype.get(newSubtype)
updateShortcutIme()
if (DEBUG) {
Log.w(TAG, "onSubtypeChanged: $currentRichInputMethodSubtype")
}
}
fun refreshSubtypeCaches() {
inputMethodInfoCache.clear()
currentRichInputMethodSubtype = RichInputMethodSubtype.get(SubtypeSettings.getSelectedSubtype(context.prefs()))
updateShortcutIme()
}
// todo: deprecated
fun switchToShortcutIme(inputMethodService: InputMethodService) {
val imiId = shortcutInputMethodInfo?.id ?: return
val token = inputMethodService.window.window?.attributes?.token ?: return
object : AsyncTask<Void?, Void?, Void?>() {
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}" } ?: "<null>"
Log.d(TAG, ("Update shortcut IME from: ${shortcutInputMethodInfo?.id ?: "<null>"}, $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}" } ?: "<null>"
Log.d(TAG, ("Update shortcut IME to: ${shortcutInputMethodInfo?.id ?: "<null>"}, $subtype"))
}
}
private fun hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes: Boolean, imiList: List<InputMethodInfo>): 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<InputMethodInfo, List<InputMethodSubtype>>()
private val cachedSubtypeListOnlyExplicitlySelected = HashMap<InputMethodInfo, List<InputMethodSubtype>>()
@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<InputMethodSubtype> {
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()
}
}