From f98754cf2d4edcde257e3e7325a24d316dd2a78e Mon Sep 17 00:00:00 2001 From: sspanak Date: Fri, 17 May 2024 16:44:40 +0300 Subject: [PATCH] automatic language selection on startup --- .../github/sspanak/tt9/ime/HotkeyHandler.java | 4 +- .../github/sspanak/tt9/ime/TypingHandler.java | 35 ++++-- .../sspanak/tt9/ime/helpers/InputField.java | 103 ++++++++++++++++++ .../sspanak/tt9/ime/helpers/TextField.java | 65 +---------- .../tt9/languages/LanguageCollection.java | 12 ++ 5 files changed, 146 insertions(+), 73 deletions(-) create mode 100644 app/src/main/java/io/github/sspanak/tt9/ime/helpers/InputField.java diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/HotkeyHandler.java b/app/src/main/java/io/github/sspanak/tt9/ime/HotkeyHandler.java index ad4c4a2a..c9bb1942 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/HotkeyHandler.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/HotkeyHandler.java @@ -303,10 +303,8 @@ public abstract class HotkeyHandler extends TypingHandler { int next = (previous + 1) % mEnabledLanguages.size(); mLanguage = LanguageCollection.getLanguage(getApplicationContext(), mEnabledLanguages.get(next)); + // validate and save it for the next time validateLanguages(); - - // save it for the next time - settings.saveInputLanguage(mLanguage.getId()); } diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/TypingHandler.java b/app/src/main/java/io/github/sspanak/tt9/ime/TypingHandler.java index 76d59f5c..8e2f53ae 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/TypingHandler.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/TypingHandler.java @@ -52,15 +52,14 @@ public abstract class TypingHandler extends KeyPadHandler { setInputField(connection, field); - // in case we are back from Settings screen, update the language list - int oldLang = mLanguage != null ? mLanguage.getId() : -1; - mEnabledLanguages = settings.getEnabledLanguageIds(); - mLanguage = LanguageCollection.getLanguage(getApplicationContext(), settings.getInputLanguage()); - validateLanguages(); + // 1. In case we are back from Settings screen, update the language list + // 2. If the connected app hints it is in a language different than the current one, + // we try to switch. + boolean languageChanged = determineLanguage(); // ignore multiple calls for the same field, caused by requestShowSelf() -> showWindow(), // or weirdly functioning apps, such as the Qin SMS app - if (restart && oldLang == mLanguage.getId() && mInputMode.getId() == getInputModeId()) { + if (restart && languageChanged && mInputMode.getId() == getInputModeId()) { return false; } @@ -88,9 +87,8 @@ public abstract class TypingHandler extends KeyPadHandler { protected void validateLanguages() { mEnabledLanguages = InputModeValidator.validateEnabledLanguages(getApplicationContext(), mEnabledLanguages); mLanguage = InputModeValidator.validateLanguage(getApplicationContext(), mLanguage, mEnabledLanguages); - - settings.saveEnabledLanguageIds(mEnabledLanguages); settings.saveInputLanguage(mLanguage.getId()); + settings.saveEnabledLanguageIds(mEnabledLanguages); } @@ -207,6 +205,27 @@ public abstract class TypingHandler extends KeyPadHandler { } + /** + * determineLanguage + * Restore the last language or auto-select a more appropriate one, if the application hints so. + * In case the settings are not valid, we will fallback to the default language. + */ + private boolean determineLanguage() { + mEnabledLanguages = settings.getEnabledLanguageIds(); + + int oldLang = mLanguage != null ? mLanguage.getId() : -1; + mLanguage = LanguageCollection.getLanguage(getApplicationContext(), settings.getInputLanguage()); + validateLanguages(); + + Language appLanguage = textField.getLanguage(getApplicationContext(), mEnabledLanguages); + if (appLanguage != null) { + mLanguage = appLanguage; + } + + return oldLang != mLanguage.getId(); + } + + /** * determineTextCase * Restore the last text case or auto-select a new one. If the InputMode supports it, it can change diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/helpers/InputField.java b/app/src/main/java/io/github/sspanak/tt9/ime/helpers/InputField.java new file mode 100644 index 00000000..8fa86d8b --- /dev/null +++ b/app/src/main/java/io/github/sspanak/tt9/ime/helpers/InputField.java @@ -0,0 +1,103 @@ +package io.github.sspanak.tt9.ime.helpers; + +import android.content.Context; +import android.os.Build; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; + +import androidx.annotation.Nullable; + +import java.util.ArrayList; + +import io.github.sspanak.tt9.languages.Language; +import io.github.sspanak.tt9.languages.LanguageCollection; + +public class InputField { + public static final int IME_ACTION_ENTER = EditorInfo.IME_MASK_ACTION + 1; + + protected final InputConnection connection; + protected final EditorInfo field; + + + protected InputField(InputConnection inputConnection, EditorInfo inputField) { + connection = inputConnection; + field = inputField; + } + + + public boolean equals(InputConnection inputConnection, EditorInfo inputField) { + return + connection != null && connection == inputConnection + && field != null && field == inputField; + } + + + /** + * getAction + * Returns the most appropriate action for the "OK" key. It could be "send", "act as ENTER key", "go (to URL)" and so on. + */ + public int getAction() { + if (field == null) { + return EditorInfo.IME_ACTION_NONE; + } + + // custom actions handling as in LatinIME. See the example in OpenBoard repo: + // https://github.com/openboard-team/openboard/blob/master/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/InputTypeUtils.java#L107 + if (field.actionId == EditorInfo.IME_ACTION_DONE || field.actionLabel != null) { + return IME_ACTION_ENTER; + } else if (field.actionId > 0) { + return field.actionId; + } + + // As in LatinIME, we want to perform an editor action, including in the case of "IME_ACTION_UNSPECIFIED". + // Otherwise, we pass through the ENTER or DPAD_CENTER key press and let the app or the system decide what to do. + // See the example below: + // https://github.com/openboard-team/openboard/blob/c3772cd56e770975ea5570db903f93b199de8b32/app/src/main/java/org/dslul/openboard/inputmethod/latin/inputlogic/InputLogic.java#L756 + int standardAction = field.imeOptions & (EditorInfo.IME_MASK_ACTION | EditorInfo.IME_FLAG_NO_ENTER_ACTION); + switch (standardAction) { + case EditorInfo.IME_ACTION_DONE: + case EditorInfo.IME_ACTION_GO: + case EditorInfo.IME_ACTION_NEXT: + case EditorInfo.IME_ACTION_PREVIOUS: + case EditorInfo.IME_ACTION_SEARCH: + case EditorInfo.IME_ACTION_SEND: + case EditorInfo.IME_ACTION_UNSPECIFIED: + return standardAction; + default: + return IME_ACTION_ENTER; + } + } + + + /** + * performAction + * Sends an action ID to the connected application. Usually, the action is determined with "this.getAction()". + * Note that it is up to the app to decide what to do or ignore the action ID. + */ + public boolean performAction(int actionId) { + return connection != null && actionId != EditorInfo.IME_ACTION_NONE && connection.performEditorAction(actionId); + } + + + /** + * getLanguage + * Detects the language hint of the current field and returns a TT9-friendly Language object. + * If the language is not supported, or the field has no hint, for example it's a numeric field or + * it's a text field where the language doesn't matter, the function returns null. + */ + @Nullable + public Language getLanguage(Context context, ArrayList allowedLanguageIds) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + return null; + } + + for (int i = 0; field.hintLocales != null && i < field.hintLocales.size(); i++) { + Language lang = LanguageCollection.getByLanguageCode(context, field.hintLocales.get(i).getLanguage()); + if (lang != null && allowedLanguageIds.contains(lang.getId())) { + return lang; + } + } + + return null; + } +} diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/helpers/TextField.java b/app/src/main/java/io/github/sspanak/tt9/ime/helpers/TextField.java index a64add5e..61c4e346 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/helpers/TextField.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/helpers/TextField.java @@ -17,32 +17,19 @@ import io.github.sspanak.tt9.languages.LanguageKind; import io.github.sspanak.tt9.util.Logger; import io.github.sspanak.tt9.util.Text; -public class TextField { - public static final int IME_ACTION_ENTER = EditorInfo.IME_MASK_ACTION + 1; - - private final InputConnection connection; - private final EditorInfo field; - - private final boolean isComposingSupported; +public class TextField extends InputField { private CharSequence composingText = ""; + private final boolean isComposingSupported; public TextField(InputConnection inputConnection, EditorInfo inputField) { - connection = inputConnection; - field = inputField; + super(inputConnection, inputField); InputType inputType = new InputType(inputConnection, inputField); isComposingSupported = !inputType.isNumeric() && !inputType.isLimited(); } - public boolean equals(InputConnection inputConnection, EditorInfo inputField) { - return - connection != null && connection == inputConnection - && field != null && field == inputField; - } - - public boolean isEmpty() { return getStringBeforeCursor(1).isEmpty() && getStringAfterCursor(1).isEmpty(); } @@ -246,50 +233,4 @@ public class TextField { return styledWord; } - - /** - * getAction - * Returns the most appropriate action for the "OK" key. It could be "send", "act as ENTER key", "go (to URL)" and so on. - */ - public int getAction() { - if (field == null) { - return EditorInfo.IME_ACTION_NONE; - } - - // custom actions handling as in LatinIME. See the example in OpenBoard repo: - // https://github.com/openboard-team/openboard/blob/master/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/InputTypeUtils.java#L107 - if (field.actionId == EditorInfo.IME_ACTION_DONE || field.actionLabel != null) { - return IME_ACTION_ENTER; - } else if (field.actionId > 0) { - return field.actionId; - } - - // As in LatinIME, we want to perform an editor action, including in the case of "IME_ACTION_UNSPECIFIED". - // Otherwise, we pass through the ENTER or DPAD_CENTER key press and let the app or the system decide what to do. - // See the example below: - // https://github.com/openboard-team/openboard/blob/c3772cd56e770975ea5570db903f93b199de8b32/app/src/main/java/org/dslul/openboard/inputmethod/latin/inputlogic/InputLogic.java#L756 - int standardAction = field.imeOptions & (EditorInfo.IME_MASK_ACTION | EditorInfo.IME_FLAG_NO_ENTER_ACTION); - switch (standardAction) { - case EditorInfo.IME_ACTION_DONE: - case EditorInfo.IME_ACTION_GO: - case EditorInfo.IME_ACTION_NEXT: - case EditorInfo.IME_ACTION_PREVIOUS: - case EditorInfo.IME_ACTION_SEARCH: - case EditorInfo.IME_ACTION_SEND: - case EditorInfo.IME_ACTION_UNSPECIFIED: - return standardAction; - default: - return IME_ACTION_ENTER; - } - - } - - /** - * performAction - * Sends an action ID to the connected application. Usually, the action is determined with "this.getAction()". - * Note that it is up to the app to decide what to do or ignore the action ID. - */ - public boolean performAction(int actionId) { - return connection != null && actionId != EditorInfo.IME_ACTION_NONE && connection.performEditorAction(actionId); - } } diff --git a/app/src/main/java/io/github/sspanak/tt9/languages/LanguageCollection.java b/app/src/main/java/io/github/sspanak/tt9/languages/LanguageCollection.java index 0a19659c..21640a59 100644 --- a/app/src/main/java/io/github/sspanak/tt9/languages/LanguageCollection.java +++ b/app/src/main/java/io/github/sspanak/tt9/languages/LanguageCollection.java @@ -8,6 +8,7 @@ import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.Locale; import io.github.sspanak.tt9.util.Logger; import io.github.sspanak.tt9.util.SystemSettings; @@ -52,6 +53,17 @@ public class LanguageCollection { return language == null ? new NullLanguage(context) : language; } + @Nullable + public static NaturalLanguage getByLanguageCode(Context context, String languageCode) { + for (NaturalLanguage lang : getInstance(context).languages.values()) { + if (lang.getLocale().getLanguage().equals(new Locale(languageCode).getLanguage())) { + return lang; + } + } + + return null; + } + @Nullable public static NaturalLanguage getByLocale(Context context, String locale) { for (NaturalLanguage lang : getInstance(context).languages.values()) {