From 9bf9e95d9717dc88c5bfce453b657b1e8ec1d81f Mon Sep 17 00:00:00 2001 From: Joshua Horton Date: Wed, 23 Sep 2026 15:53:30 -0500 Subject: [PATCH] fix(android): selectively disable autocorrect based on context intent Fixes: #16614 Build-bot: skip release:web,android Notable changes: - Adds public (API) `KMManager.setBannerOptions(SuggestionType)` overload - Adds public (API) `KMManager.setBannerOptions(SuggestionType, KeyboardType)` overload - Adds public (API) `getAutocorrectionsSuspended`, `setAutocorrectionsSuspended` using the same format and style as `the `predictionsSuspended` API added in 18.0 --- .../com/keyman/android/SystemKeyboard.java | 1 + .../com/tavultesoft/kmapro/MainActivity.java | 1 + .../KMEA/app/src/main/assets/android-host.js | 11 +- .../java/com/keyman/engine/KMKeyboard.java | 10 ++ .../engine/KMKeyboardWebViewClient.java | 16 +- .../java/com/keyman/engine/KMManager.java | 161 +++++++++++++++--- 6 files changed, 159 insertions(+), 41 deletions(-) diff --git a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java index c3939c03f8..40b6381a65 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java +++ b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java @@ -172,6 +172,7 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven Context appContext = getApplicationContext(); // Temporarily disable predictions on certain fields (e.g. hidden password field or numeric) inputType = attribute.inputType; + KMManager.setAutocorrectionsSuspended(inputType, KeyboardType.KEYBOARD_TYPE_SYSTEM); KMManager.setPredictionsSuspended(inputType, KeyboardType.KEYBOARD_TYPE_SYSTEM); if (KMManager.getPredictionsSuspended(KeyboardType.KEYBOARD_TYPE_SYSTEM)) { KMManager.setBannerOptions(false); diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java index 044f21abba..36b883c872 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java @@ -444,6 +444,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene } if (textView != null) { + KMManager.setAutocorrectionsSuspended(textView.getInputType(), KeyboardType.KEYBOARD_TYPE_INAPP); // Reset inAppPredictionsSuspendedForSensitiveInput flag KMManager.setPredictionsSuspended(textView.getInputType(), KeyboardType.KEYBOARD_TYPE_INAPP); } diff --git a/android/KMEA/app/src/main/assets/android-host.js b/android/KMEA/app/src/main/assets/android-host.js index 883b9b6bb5..4728caf0c1 100644 --- a/android/KMEA/app/src/main/assets/android-host.js +++ b/android/KMEA/app/src/main/assets/android-host.js @@ -259,6 +259,11 @@ function enableSuggestions(model, suggestionType) { // the moment we actually register the new model. // Use console_debug console_debug('enableSuggestions(model, maySuggest='+suggestionType+')'); + setBannerOptions(suggestionType); + registerModel(model); +} + +function setBannerOptions(suggestionType) { const suggestionSettings = [ // mayPredict, mayCorrect, mayAutoCorrect [false, false, false], // 0 = SuggestionType.SUGGESTIONS_DISABLED @@ -271,12 +276,6 @@ function enableSuggestions(model, suggestionType) { keyman.core.languageProcessor.mayPredict = t[0]; keyman.core.languageProcessor.maySuggest = t[1]; keyman.core.languageProcessor.mayAutoCorrect = t[2]; - - registerModel(model); -} - -function setBannerOptions(mayPredict) { - keyman.core.languageProcessor.mayPredict = mayPredict; } function registerModel(model) { diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java index bffb0c1307..51cea2f0f8 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java @@ -33,6 +33,7 @@ import android.content.res.Configuration; import android.net.Uri; import android.os.Handler; import android.os.Looper; +import android.text.InputType; import android.util.AndroidRuntimeException; import android.util.DisplayMetrics; import android.util.Log; @@ -452,6 +453,15 @@ final class KMKeyboard extends WebView { public void onResume() { DisplayMetrics dms = context.getResources().getDisplayMetrics(); int kbWidth = (int) (dms.widthPixels / dms.density); + // Refresh the setting whenever the view is reloaded; + // we may have swapped between app and system keyboards. + if(this.keyboardType == KeyboardType.KEYBOARD_TYPE_INAPP) { + // Within the main engine, we'll assume that it's always used for text. + // We don't currently support a way to adjust that for the in-app keyboard. + KMManager.setPredictionsSuspended(InputType.TYPE_CLASS_TEXT, this.keyboardType); + KMManager.setAutocorrectionsSuspended(InputType.TYPE_CLASS_TEXT, this.keyboardType); + } + KMManager.setBannerOptions(true); // Get the correct keyboard height for current orientation int kbHeight = KMManager.getKeyboardHeight(context); diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java index f043a56681..e6cd2dde08 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java @@ -6,7 +6,6 @@ package com.keyman.engine; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Bitmap; -import android.graphics.RectF; import android.net.Uri; import android.util.Log; import android.webkit.WebResourceRequest; @@ -18,18 +17,13 @@ import androidx.webkit.WebViewAssetLoader; import androidx.webkit.WebViewAssetLoader.InternalStoragePathHandler; import com.keyman.engine.KeyboardEventHandler.EventType; -import com.keyman.engine.KMManager; import com.keyman.engine.KMManager.KeyboardType; -import com.keyman.engine.KMManager.SuggestionType; import com.keyman.engine.util.KMLog; import com.keyman.engine.data.Keyboard; import com.keyman.engine.util.WebViewUtils; import org.json.JSONObject; -import java.util.ArrayList; -import java.util.HashMap; - public final class KMKeyboardWebViewClient extends WebViewClient { public static final String TAG = "KMKeyboardWebViewClient"; @@ -175,15 +169,7 @@ public final class KMKeyboardWebViewClient extends WebViewClient { // for the rest of the lifetime of this keyboard instance. kmKeyboard.setShouldShowHelpBubble(false); } else if (url.indexOf("refreshBannerHeight") >= 0) { - // appContext instead of context? - SharedPreferences prefs = context.getSharedPreferences(context.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); - boolean modelPredictionPref = false; - if (!KMManager.getPredictionsSuspended(keyboardType) && KMManager.currentLexicalModel != null) { - modelPredictionPref = prefs.getInt(KMManager.getLanguageAutoCorrectionPreferenceKey( - KMManager.currentLexicalModel.get(KMManager.KMKey_LanguageID)), KMManager.KMDefault_Suggestion) - != SuggestionType.SUGGESTIONS_DISABLED.toInt(); - } - KMManager.setBannerOptions(modelPredictionPref, keyboardType); + KMManager.setBannerOptions(KMManager.determinePredictionConfig(context, keyboardType), keyboardType); RelativeLayout.LayoutParams params = KMManager.getKeyboardLayoutParams(); kmKeyboard.setLayoutParams(params); } else if (url.indexOf("suggestPopup") >= 0) { diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java index 80701ad683..645907d37f 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java @@ -42,7 +42,6 @@ import android.view.Display; import android.view.Surface; import android.view.ViewGroup; import android.view.Window; -import android.view.WindowInsets; import android.view.WindowManager; import android.view.WindowMetrics; import android.view.inputmethod.EditorInfo; @@ -54,7 +53,6 @@ import android.widget.RelativeLayout; import androidx.core.content.ContextCompat; import androidx.core.graphics.Insets; -import androidx.core.view.DisplayCutoutCompat; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; @@ -80,7 +78,6 @@ import com.keyman.engine.util.KMLog; import com.keyman.engine.util.KMString; import com.keyman.engine.util.MapCompat; import com.keyman.engine.util.WebViewUtils; -import com.keyman.engine.util.WebViewUtils.SystemWebViewStatus; import org.json.JSONArray; import org.json.JSONException; @@ -273,6 +270,13 @@ public final class KMManager { private static boolean inAppPredictionsSuspendedForSensitiveInput = false; private static boolean systemPredictionsSuspendedForSensitiveInput = false; + // Special flags to temporarily disable autocorrect when editing fields it + // would interfere with. These are maintained independently for inapp keyboard + // and system keyboard. + // API added Keyman 19.0 + private static boolean inAppAutocorrectionsSuspended = false; + private static boolean systemAutocorrectionsSuspended = false; + // Determine how system keyboard handles ENTER key public static EnterModeType enterMode = EnterModeType.DEFAULT; @@ -1535,6 +1539,7 @@ public final class KMManager { private static boolean shouldSuspendPredictions(int inputType) { return ((inputType == (InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD)) || (inputType == (InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD)) || + (inputType == (InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS)) || isNumericField(inputType)); } @@ -1571,6 +1576,62 @@ public final class KMManager { return false; } + private static boolean shouldSuspendAutocorrections(int inputType) { + // Do not autocorrect contexts where text-format data is not expected. + // (Disable for Numbers and DateTime formats.) + if((inputType & InputType.TYPE_CLASS_TEXT) == 0) { + return true; + } + + switch(inputType & InputType.TYPE_MASK_VARIATION) { + case InputType.TYPE_TEXT_VARIATION_FILTER: + case InputType.TYPE_TEXT_VARIATION_NORMAL: + case InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE: + case InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE: + case InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT: + case InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT: + return false; + + // TODO: return false for the following case once epic/user-dict lands! + case InputType.TYPE_TEXT_VARIATION_PERSON_NAME: + default: + return true; + } + } + + // Do within the setPredictionsSuspended part, probably? + /** + * Sets autocorrectionsSuspended true if the given InputType field for the keyboard + * should not be corrected, as with email and address/URL bar fields. + * Also true for numeric text fields. + * This is to temporarily disable automatic application of suggestions in these text fields. + * API added Keyman 18.0 + * @param inputType android.text.InputType + * @param {KeyboardType} keyboard + */ + public static void setAutocorrectionsSuspended(int inputType, KeyboardType keyboard) { + if (keyboard == KeyboardType.KEYBOARD_TYPE_INAPP) { + inAppAutocorrectionsSuspended = shouldSuspendAutocorrections(inputType); + } else if (keyboard == KeyboardType.KEYBOARD_TYPE_SYSTEM) { + systemAutocorrectionsSuspended = shouldSuspendAutocorrections(inputType); + } + } + + /** + * Get the value of autocorrectionsSuspended based on KeyboardType. + * API added in Keyman 18.0 + * @param {KeyboardType} keyboard + * @return boolean of autocorrectionsSuspended or systemPredictionsSuspendedForSensitiveInput + */ + public static boolean getAutocorrectionsSuspended(KeyboardType keyboard) { + if (keyboard == KeyboardType.KEYBOARD_TYPE_INAPP) { + return inAppAutocorrectionsSuspended; + } else if (keyboard == KeyboardType.KEYBOARD_TYPE_SYSTEM) { + return systemAutocorrectionsSuspended; + } + return false; + } + /** * Store SuggestionType as int in preference * @param languageID as String @@ -1706,26 +1767,15 @@ public final class KMManager { model = model.replaceAll("\'", "\\\\'"); // Double-escaped-backslash b/c regex. model = model.replaceAll("\"", "'"); - // When entering password field, maySuggest should override to disabled - SharedPreferences prefs = appContext.getSharedPreferences(KMManager.KMEngine_PrefsKey, Context.MODE_PRIVATE); - int suggestionPreference = prefs.getInt(getLanguageAutoCorrectionPreferenceKey(languageID), KMDefault_Suggestion); - - RelativeLayout.LayoutParams params; + currentLexicalModel = lexicalModelInfo; + // *** refactor to use common settings-detection with the setBannerOptions route. *** if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_INAPP) && !InAppKeyboard.shouldIgnoreTextChange() && modelFileExists) { - params = getKeyboardLayoutParams(); - // Do NOT re-layout here; it'll be triggered once the banner loads. - int inappMaySuggest = inAppPredictionsSuspendedForSensitiveInput ? SuggestionType.SUGGESTIONS_DISABLED.toInt() : - suggestionPreference; - InAppKeyboard.loadJavascript(KMString.format("enableSuggestions(%s, %d)", model, inappMaySuggest)); + InAppKeyboard.loadJavascript(KMString.format("enableSuggestions(%s, %d)", model, determinePredictionConfig(appContext, KeyboardType.KEYBOARD_TYPE_INAPP).toInt())); } if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_SYSTEM) && !SystemKeyboard.shouldIgnoreTextChange() && modelFileExists) { - params = getKeyboardLayoutParams(); - // Do NOT re-layout here; it'll be triggered once the banner loads. - int maySuggest = systemPredictionsSuspendedForSensitiveInput ? SuggestionType.SUGGESTIONS_DISABLED.toInt() : - suggestionPreference; - SystemKeyboard.loadJavascript(KMString.format("enableSuggestions(%s, %d)", model, maySuggest)); + SystemKeyboard.loadJavascript(KMString.format("enableSuggestions(%s, %d)", model, determinePredictionConfig(appContext, KeyboardType.KEYBOARD_TYPE_SYSTEM).toInt())); } return true; } @@ -1757,6 +1807,34 @@ public final class KMManager { KeyboardPickerActivity.deleteLexicalModel(context, position, silenceNotification); } + static SuggestionType determinePredictionConfig(Context context, KeyboardType keyboardType) { + SharedPreferences prefs = context.getSharedPreferences(context.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); + + // When entering password field, maySuggest should override to disabled + if (KMManager.getPredictionsSuspended(keyboardType) || KMManager.currentLexicalModel == null) { + return SuggestionType.SUGGESTIONS_DISABLED; + } + + SuggestionType selectedType = SuggestionType.fromInt( + prefs.getInt( + KMManager.getLanguageAutoCorrectionPreferenceKey( + KMManager.currentLexicalModel.get(KMManager.KMKey_LanguageID) + ), + KMManager.KMDefault_Suggestion) + ); + + if(selectedType == SuggestionType.SUGGESTIONS_DISABLED) { + return selectedType; + } + + if (KMManager.getAutocorrectionsSuspended(keyboardType) && selectedType == SuggestionType.PREDICTIONS_WITH_AUTO_CORRECT) { + // Suspend autocorrection while still allowing corrections. + return SuggestionType.PREDICTIONS_WITH_CORRECTIONS; + } + + return selectedType; + } + /** * setBannerOptions - Update KMW whether to generate predictions. * For now, also display banner @@ -1764,7 +1842,27 @@ public final class KMManager { * @return boolean - Success */ public static boolean setBannerOptions(boolean mayPredict) { - String url = KMString.format("setBannerOptions(%s)", mayPredict); + setBannerOptions( + mayPredict ? determinePredictionConfig(appContext, KeyboardType.KEYBOARD_TYPE_INAPP) : SuggestionType.SUGGESTIONS_DISABLED, + KeyboardType.KEYBOARD_TYPE_INAPP + ); + setBannerOptions( + mayPredict ? determinePredictionConfig(appContext, KeyboardType.KEYBOARD_TYPE_SYSTEM) : SuggestionType.SUGGESTIONS_DISABLED, + KeyboardType.KEYBOARD_TYPE_SYSTEM + ); + + return true; + } + + /** + * setBannerOptions - Update KMW whether to generate predictions. + * For now, also display banner + * @param predictSetting - the SuggestionType that should be utilized for the current input Context + * @return boolean - Success + */ + public static boolean setBannerOptions(SuggestionType predictSetting) { + // why here and not within the WebViewClient class? + String url = KMString.format("setBannerOptions(%s)", predictSetting.toInt()); if (InAppKeyboard != null) { InAppKeyboard.loadJavascript(url); } @@ -1784,7 +1882,30 @@ public final class KMManager { * @return boolean - Success */ public static boolean setBannerOptions(boolean mayPredict, KeyboardType keyboard) { - String url = KMString.format("setBannerOptions(%s)", mayPredict); + if (keyboard == KeyboardType.KEYBOARD_TYPE_INAPP && InAppKeyboard != null) { + SuggestionType predictConfig = mayPredict ? determinePredictionConfig(appContext, KeyboardType.KEYBOARD_TYPE_INAPP) : SuggestionType.SUGGESTIONS_DISABLED; + String url = KMString.format("setBannerOptions(%s)", predictConfig); + InAppKeyboard.loadJavascript(url); + } + + if (keyboard == KeyboardType.KEYBOARD_TYPE_SYSTEM && SystemKeyboard != null) { + SuggestionType predictConfig = mayPredict ? determinePredictionConfig(appContext, KeyboardType.KEYBOARD_TYPE_SYSTEM) : SuggestionType.SUGGESTIONS_DISABLED; + String url = KMString.format("setBannerOptions(%s)", predictConfig); + SystemKeyboard.loadJavascript(url); + } + + return true; + } + + /** + * setBannerOptions - Update KMW for inapp/system keyboard whether to generate predictions. + * For now, also display banner + * @param predictSetting - the SuggestionType that should be utilized for the current input Context + * @param {KeyboardType} keyboard + * @return boolean - Success + */ + public static boolean setBannerOptions(SuggestionType predictSetting, KeyboardType keyboard) { + String url = KMString.format("setBannerOptions(%s)", predictSetting.toInt()); if (keyboard == KeyboardType.KEYBOARD_TYPE_INAPP && InAppKeyboard != null) { InAppKeyboard.loadJavascript(url); }