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
This commit is contained in:
Joshua Horton 2026-09-23 15:53:30 -05:00
parent 298fa3b233
commit 9bf9e95d97
6 changed files with 159 additions and 41 deletions

View file

@ -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);

View file

@ -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);
}

View file

@ -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) {

View file

@ -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);

View file

@ -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) {

View file

@ -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);
}