mirror of
https://github.com/Helium314/HeliBoard.git
synced 2025-06-03 13:20:31 +00:00
move RichInputMethodManager to Kotlin
This commit is contained in:
parent
424df5fb0d
commit
09eecc0502
2 changed files with 329 additions and 445 deletions
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue