/* * Copyright (C) 2008 The Android Open Source Project * modified * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only */ package helium314.keyboard.latin; import android.annotation.SuppressLint; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Color; import android.inputmethodservice.InputMethodService; import android.media.AudioManager; import android.os.Build; import android.os.Bundle; import android.os.Debug; import android.os.IBinder; import android.os.Message; import android.os.Process; import android.text.InputType; import android.util.PrintWriterPrinter; import android.util.Printer; import android.util.SparseArray; import android.view.Gravity; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup.LayoutParams; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InlineSuggestion; import android.view.inputmethod.InlineSuggestionsRequest; import android.view.inputmethod.InlineSuggestionsResponse; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; import helium314.keyboard.accessibility.AccessibilityUtils; import helium314.keyboard.compat.ConfigurationCompatKt; import helium314.keyboard.compat.EditorInfoCompatUtils; import helium314.keyboard.keyboard.KeyboardActionListener; import helium314.keyboard.keyboard.KeyboardActionListenerImpl; import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode; import helium314.keyboard.latin.common.InsetsOutlineProvider; import helium314.keyboard.dictionarypack.DictionaryPackConstants; import helium314.keyboard.event.Event; import helium314.keyboard.event.HangulEventDecoder; import helium314.keyboard.event.HardwareEventDecoder; import helium314.keyboard.event.HardwareKeyboardEventDecoder; import helium314.keyboard.event.InputTransaction; import helium314.keyboard.keyboard.Keyboard; import helium314.keyboard.keyboard.KeyboardId; import helium314.keyboard.keyboard.KeyboardLayoutSet; import helium314.keyboard.keyboard.KeyboardSwitcher; import helium314.keyboard.keyboard.MainKeyboardView; import helium314.keyboard.latin.Suggest.OnGetSuggestedWordsCallback; import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo; import helium314.keyboard.latin.common.ColorType; import helium314.keyboard.latin.common.Constants; import helium314.keyboard.latin.common.CoordinateUtils; import helium314.keyboard.latin.common.InputPointers; import helium314.keyboard.latin.common.LocaleUtils; import helium314.keyboard.latin.common.ViewOutlineProviderUtilsKt; import helium314.keyboard.latin.define.DebugFlags; import helium314.keyboard.latin.define.ProductionFlags; import helium314.keyboard.latin.inputlogic.InputLogic; import helium314.keyboard.latin.permissions.PermissionsManager; import helium314.keyboard.latin.personalization.PersonalizationHelper; import helium314.keyboard.latin.settings.Settings; import helium314.keyboard.latin.settings.SettingsActivity; import helium314.keyboard.latin.settings.SettingsValues; import helium314.keyboard.latin.suggestions.SuggestionStripView; import helium314.keyboard.latin.suggestions.SuggestionStripViewAccessor; import helium314.keyboard.latin.touchinputconsumer.GestureConsumer; import helium314.keyboard.latin.utils.ColorUtilKt; import helium314.keyboard.latin.utils.InlineAutofillUtils; import helium314.keyboard.latin.utils.InputMethodPickerKt; import helium314.keyboard.latin.utils.JniUtils; import helium314.keyboard.latin.utils.LeakGuardHandlerWrapper; import helium314.keyboard.latin.utils.Log; import helium314.keyboard.latin.utils.StatsUtils; import helium314.keyboard.latin.utils.StatsUtilsManager; import helium314.keyboard.latin.utils.SubtypeLocaleUtils; import helium314.keyboard.latin.utils.SubtypeSettingsKt; import helium314.keyboard.latin.utils.ViewLayoutUtils; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.concurrent.TimeUnit; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; /** * Input method implementation for Qwerty'ish keyboard. */ public class LatinIME extends InputMethodService implements SuggestionStripView.Listener, SuggestionStripViewAccessor, DictionaryFacilitator.DictionaryInitializationListener, PermissionsManager.PermissionsResultCallback { static final String TAG = LatinIME.class.getSimpleName(); private static final boolean TRACE = false; private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100; private static final int PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT = 2; private static final int PENDING_IMS_CALLBACK_DURATION_MILLIS = 800; static final long DELAY_WAIT_FOR_DICTIONARY_LOAD_MILLIS = TimeUnit.SECONDS.toMillis(2); static final long DELAY_DEALLOCATE_MEMORY_MILLIS = TimeUnit.SECONDS.toMillis(10); /** * The name of the scheme used by the Package Manager to warn of a new package installation, * replacement or removal. */ private static final String SCHEME_PACKAGE = "package"; final Settings mSettings; public final KeyboardActionListener mKeyboardActionListener; private int mOriginalNavBarColor = 0; private int mOriginalNavBarFlags = 0; private final DictionaryFacilitator mDictionaryFacilitator = DictionaryFacilitatorProvider.getDictionaryFacilitator(false); final InputLogic mInputLogic = new InputLogic(this, this, mDictionaryFacilitator); // We expect to have only one decoder in almost all cases, hence the default capacity of 1. // If it turns out we need several, it will get grown seamlessly. final SparseArray mHardwareEventDecoders = new SparseArray<>(1); // TODO: Move these {@link View}s to {@link KeyboardSwitcher}. private View mInputView; private InsetsOutlineProvider mInsetsUpdater; private SuggestionStripView mSuggestionStripView; private RichInputMethodManager mRichImm; final KeyboardSwitcher mKeyboardSwitcher; private final SubtypeState mSubtypeState = new SubtypeState(); private EmojiAltPhysicalKeyDetector mEmojiAltPhysicalKeyDetector; private final StatsUtilsManager mStatsUtilsManager; // Working variable for {@link #startShowingInputView()} and // {@link #onEvaluateInputViewShown()}. private boolean mIsExecutingStartShowingInputView; // Used for re-initialize keyboard layout after onConfigurationChange. @Nullable private Context mDisplayContext; // Object for reacting to adding/removing a dictionary pack. private final BroadcastReceiver mDictionaryPackInstallReceiver = new DictionaryPackInstallBroadcastReceiver(this); private final BroadcastReceiver mDictionaryDumpBroadcastReceiver = new DictionaryDumpBroadcastReceiver(this); final static class RestartAfterDeviceUnlockReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); // Restart the keyboard if credential encrypted storage is unlocked. This reloads the // dictionary and other data from credential-encrypted storage (with the onCreate() // method). if (Intent.ACTION_USER_UNLOCKED.equals(action)) { final int myPid = Process.myPid(); Log.i(TAG, "Killing my process: pid=" + myPid); Process.killProcess(myPid); } else { Log.e(TAG, "Unexpected intent " + intent); } } } final RestartAfterDeviceUnlockReceiver mRestartAfterDeviceUnlockReceiver = new RestartAfterDeviceUnlockReceiver(); private AlertDialog mOptionsDialog; private final boolean mIsHardwareAcceleratedDrawingEnabled; private GestureConsumer mGestureConsumer = GestureConsumer.NULL_GESTURE_CONSUMER; private final ClipboardHistoryManager mClipboardHistoryManager = new ClipboardHistoryManager(this); public final UIHandler mHandler = new UIHandler(this); public static final class UIHandler extends LeakGuardHandlerWrapper { private static final int MSG_UPDATE_SHIFT_STATE = 0; private static final int MSG_PENDING_IMS_CALLBACK = 1; private static final int MSG_UPDATE_SUGGESTION_STRIP = 2; private static final int MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 3; private static final int MSG_RESUME_SUGGESTIONS = 4; private static final int MSG_REOPEN_DICTIONARIES = 5; private static final int MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED = 6; private static final int MSG_RESET_CACHES = 7; private static final int MSG_WAIT_FOR_DICTIONARY_LOAD = 8; private static final int MSG_DEALLOCATE_MEMORY = 9; private static final int MSG_SWITCH_LANGUAGE_AUTOMATICALLY = 10; // Update this when adding new messages private static final int MSG_LAST = MSG_SWITCH_LANGUAGE_AUTOMATICALLY; private static final int ARG1_NOT_GESTURE_INPUT = 0; private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1; private static final int ARG1_SHOW_GESTURE_FLOATING_PREVIEW_TEXT = 2; private static final int ARG2_UNUSED = 0; private static final int ARG1_TRUE = 1; private int mDelayInMillisecondsToUpdateSuggestions; private int mDelayInMillisecondsToUpdateShiftState; public UIHandler(@NonNull final LatinIME ownerInstance) { super(ownerInstance); } public void onCreate() { final LatinIME latinIme = getOwnerInstance(); if (latinIme == null) { return; } final Resources res = latinIme.getResources(); mDelayInMillisecondsToUpdateSuggestions = res.getInteger( R.integer.config_delay_in_milliseconds_to_update_suggestions); mDelayInMillisecondsToUpdateShiftState = res.getInteger( R.integer.config_delay_in_milliseconds_to_update_shift_state); } @Override public void handleMessage(@NonNull final Message msg) { final LatinIME latinIme = getOwnerInstance(); if (latinIme == null) { return; } switch (msg.what) { case MSG_UPDATE_SUGGESTION_STRIP: cancelUpdateSuggestionStrip(); latinIme.mInputLogic.performUpdateSuggestionStripSync( latinIme.mSettings.getCurrent(), msg.arg1 /* inputStyle */); break; case MSG_UPDATE_SHIFT_STATE: latinIme.mKeyboardSwitcher.requestUpdatingShiftState(latinIme.getCurrentAutoCapsState(), latinIme.getCurrentRecapitalizeState()); break; case MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP: if (msg.arg1 == ARG1_NOT_GESTURE_INPUT) { final SuggestedWords suggestedWords = (SuggestedWords) msg.obj; latinIme.showSuggestionStrip(suggestedWords); } else { latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords) msg.obj, msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT); } break; case MSG_RESUME_SUGGESTIONS: latinIme.mInputLogic.restartSuggestionsOnWordTouchedByCursor( latinIme.mSettings.getCurrent(), latinIme.mKeyboardSwitcher.getCurrentKeyboardScript()); break; case MSG_REOPEN_DICTIONARIES: // We need to re-evaluate the currently composing word in case the script has // changed. postWaitForDictionaryLoad(); latinIme.resetDictionaryFacilitatorIfNecessary(); break; case MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED: final SuggestedWords suggestedWords = (SuggestedWords) msg.obj; latinIme.mInputLogic.onUpdateTailBatchInputCompleted( latinIme.mSettings.getCurrent(), suggestedWords, latinIme.mKeyboardSwitcher); latinIme.onTailBatchInputResultShown(suggestedWords); break; case MSG_RESET_CACHES: final SettingsValues settingsValues = latinIme.mSettings.getCurrent(); if (latinIme.mInputLogic.retryResetCachesAndReturnSuccess( msg.arg1 == ARG1_TRUE /* tryResumeSuggestions */, msg.arg2 /* remainingTries */, this /* handler */)) { // If we were able to reset the caches, then we can reload the keyboard. // Otherwise, we'll do it when we can. latinIme.mKeyboardSwitcher.loadKeyboard(latinIme.getCurrentInputEditorInfo(), settingsValues, latinIme.getCurrentAutoCapsState(), latinIme.getCurrentRecapitalizeState()); } break; case MSG_WAIT_FOR_DICTIONARY_LOAD: Log.i(TAG, "Timeout waiting for dictionary load"); break; case MSG_DEALLOCATE_MEMORY: latinIme.deallocateMemory(); break; case MSG_SWITCH_LANGUAGE_AUTOMATICALLY: latinIme.switchToSubtype((InputMethodSubtype) msg.obj); break; } } public void postUpdateSuggestionStrip(final int inputStyle) { sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION_STRIP, inputStyle, 0 /* ignored */), mDelayInMillisecondsToUpdateSuggestions); } public void postReopenDictionaries() { sendMessage(obtainMessage(MSG_REOPEN_DICTIONARIES)); } public void postResumeSuggestions(final boolean shouldDelay) { final LatinIME latinIme = getOwnerInstance(); if (latinIme == null) { return; } if (!latinIme.mSettings.getCurrent().isSuggestionsEnabledPerUserSettings()) { return; } removeMessages(MSG_RESUME_SUGGESTIONS); final int message = MSG_RESUME_SUGGESTIONS; if (shouldDelay) { sendMessageDelayed(obtainMessage(message), mDelayInMillisecondsToUpdateSuggestions); } else { sendMessage(obtainMessage(message)); } } public void postResetCaches(final boolean tryResumeSuggestions, final int remainingTries) { removeMessages(MSG_RESET_CACHES); sendMessage(obtainMessage(MSG_RESET_CACHES, tryResumeSuggestions ? 1 : 0, remainingTries, null)); } public void postWaitForDictionaryLoad() { sendMessageDelayed(obtainMessage(MSG_WAIT_FOR_DICTIONARY_LOAD), DELAY_WAIT_FOR_DICTIONARY_LOAD_MILLIS); } public void cancelWaitForDictionaryLoad() { removeMessages(MSG_WAIT_FOR_DICTIONARY_LOAD); } public boolean hasPendingWaitForDictionaryLoad() { return hasMessages(MSG_WAIT_FOR_DICTIONARY_LOAD); } public void cancelUpdateSuggestionStrip() { removeMessages(MSG_UPDATE_SUGGESTION_STRIP); } public void cancelResumeSuggestions() { removeMessages(MSG_RESUME_SUGGESTIONS); } public boolean hasPendingUpdateSuggestions() { return hasMessages(MSG_UPDATE_SUGGESTION_STRIP); } public boolean hasPendingReopenDictionaries() { return hasMessages(MSG_REOPEN_DICTIONARIES); } public void postUpdateShiftState() { removeMessages(MSG_UPDATE_SHIFT_STATE); sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), mDelayInMillisecondsToUpdateShiftState); } public void postDeallocateMemory() { sendMessageDelayed(obtainMessage(MSG_DEALLOCATE_MEMORY), DELAY_DEALLOCATE_MEMORY_MILLIS); } public void cancelDeallocateMemory() { removeMessages(MSG_DEALLOCATE_MEMORY); } public boolean hasPendingDeallocateMemory() { return hasMessages(MSG_DEALLOCATE_MEMORY); } public void removeAllMessages() { for (int i = 0; i <= MSG_LAST; ++i) { removeMessages(i); } } public void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords, final boolean dismissGestureFloatingPreviewText) { removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); final int arg1 = dismissGestureFloatingPreviewText ? ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT : ARG1_SHOW_GESTURE_FLOATING_PREVIEW_TEXT; obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, arg1, ARG2_UNUSED, suggestedWords).sendToTarget(); } public void showSuggestionStrip(final SuggestedWords suggestedWords) { removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, ARG1_NOT_GESTURE_INPUT, ARG2_UNUSED, suggestedWords).sendToTarget(); } public void showTailBatchInputResult(final SuggestedWords suggestedWords) { obtainMessage(MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED, suggestedWords).sendToTarget(); } public void postSwitchLanguage(final InputMethodSubtype subtype) { obtainMessage(MSG_SWITCH_LANGUAGE_AUTOMATICALLY, subtype).sendToTarget(); } // Working variables for the following methods. private boolean mIsOrientationChanging; private boolean mPendingSuccessiveImsCallback; private boolean mHasPendingStartInput; private boolean mHasPendingFinishInputView; private boolean mHasPendingFinishInput; private EditorInfo mAppliedEditorInfo; public void startOrientationChanging() { removeMessages(MSG_PENDING_IMS_CALLBACK); resetPendingImsCallback(); mIsOrientationChanging = true; final LatinIME latinIme = getOwnerInstance(); if (latinIme == null) { return; } if (latinIme.isInputViewShown()) { latinIme.mKeyboardSwitcher.saveKeyboardState(); } } private void resetPendingImsCallback() { mHasPendingFinishInputView = false; mHasPendingFinishInput = false; mHasPendingStartInput = false; } private void executePendingImsCallback(final LatinIME latinIme, final EditorInfo editorInfo, boolean restarting) { if (mHasPendingFinishInputView) { latinIme.onFinishInputViewInternal(mHasPendingFinishInput); } if (mHasPendingFinishInput) { latinIme.onFinishInputInternal(); } if (mHasPendingStartInput) { latinIme.onStartInputInternal(editorInfo, restarting); } resetPendingImsCallback(); } public void onStartInput(final EditorInfo editorInfo, final boolean restarting) { if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { // Typically this is the second onStartInput after orientation changed. mHasPendingStartInput = true; } else { if (mIsOrientationChanging && restarting) { // This is the first onStartInput after orientation changed. mIsOrientationChanging = false; mPendingSuccessiveImsCallback = true; } final LatinIME latinIme = getOwnerInstance(); if (latinIme != null) { executePendingImsCallback(latinIme, editorInfo, restarting); latinIme.onStartInputInternal(editorInfo, restarting); } } } public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) { if (hasMessages(MSG_PENDING_IMS_CALLBACK) && KeyboardId.equivalentEditorInfoForKeyboard(editorInfo, mAppliedEditorInfo)) { // Typically this is the second onStartInputView after orientation changed. resetPendingImsCallback(); } else { if (mPendingSuccessiveImsCallback) { // This is the first onStartInputView after orientation changed. mPendingSuccessiveImsCallback = false; resetPendingImsCallback(); sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK), PENDING_IMS_CALLBACK_DURATION_MILLIS); } final LatinIME latinIme = getOwnerInstance(); if (latinIme != null) { executePendingImsCallback(latinIme, editorInfo, restarting); latinIme.onStartInputViewInternal(editorInfo, restarting); mAppliedEditorInfo = editorInfo; } cancelDeallocateMemory(); } } public void onFinishInputView(final boolean finishingInput) { if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { // Typically this is the first onFinishInputView after orientation changed. mHasPendingFinishInputView = true; } else { final LatinIME latinIme = getOwnerInstance(); if (latinIme != null) { latinIme.onFinishInputViewInternal(finishingInput); mAppliedEditorInfo = null; } if (!hasPendingDeallocateMemory()) { postDeallocateMemory(); } } } public void onFinishInput() { if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { // Typically this is the first onFinishInput after orientation changed. mHasPendingFinishInput = true; } else { final LatinIME latinIme = getOwnerInstance(); if (latinIme != null) { executePendingImsCallback(latinIme, null, false); latinIme.onFinishInputInternal(); } } } } final class SubtypeState { private InputMethodSubtype mLastActiveSubtype; private boolean mCurrentSubtypeHasBeenUsed = true; // starting with true avoids immediate switch public void setCurrentSubtypeHasBeenUsed() { mCurrentSubtypeHasBeenUsed = true; } public void switchSubtype(final RichInputMethodManager richImm) { final InputMethodSubtype currentSubtype = richImm.getCurrentSubtype().getRawSubtype(); final InputMethodSubtype lastActiveSubtype = mLastActiveSubtype; final boolean currentSubtypeHasBeenUsed = mCurrentSubtypeHasBeenUsed; if (currentSubtypeHasBeenUsed) { mLastActiveSubtype = currentSubtype; mCurrentSubtypeHasBeenUsed = false; } if (currentSubtypeHasBeenUsed && richImm.checkIfSubtypeBelongsToThisImeAndEnabled(lastActiveSubtype) && !currentSubtype.equals(lastActiveSubtype)) { switchToSubtype(lastActiveSubtype); return; } // switchSubtype is called only for internal switching, so let's just switch to the next subtype switchToSubtype(richImm.getNextSubtypeInThisIme(true)); } } // Loading the native library eagerly to avoid unexpected UnsatisfiedLinkError at the initial // JNI call as much as possible. static { JniUtils.loadNativeLibrary(); } public LatinIME() { super(); mSettings = Settings.getInstance(); mKeyboardSwitcher = KeyboardSwitcher.getInstance(); mStatsUtilsManager = StatsUtilsManager.getInstance(); mKeyboardActionListener = new KeyboardActionListenerImpl(this, mInputLogic); mIsHardwareAcceleratedDrawingEnabled = this.enableHardwareAcceleration(); Log.i(TAG, "Hardware accelerated drawing: " + mIsHardwareAcceleratedDrawingEnabled); } @Override public void onCreate() { Settings.init(this); DebugFlags.init(this); SubtypeSettingsKt.init(this); RichInputMethodManager.init(this); mRichImm = RichInputMethodManager.getInstance(); AudioAndHapticFeedbackManager.init(this); AccessibilityUtils.init(this); mStatsUtilsManager.onCreate(this, mDictionaryFacilitator); mDisplayContext = getDisplayContext(); KeyboardSwitcher.init(this); super.onCreate(); mClipboardHistoryManager.onCreate(); mHandler.onCreate(); loadSettings(); // Register to receive ringer mode change. final IntentFilter filter = new IntentFilter(); filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); registerReceiver(mRingerModeChangeReceiver, filter); // Register to receive installation and removal of a dictionary pack. final IntentFilter packageFilter = new IntentFilter(); packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); packageFilter.addDataScheme(SCHEME_PACKAGE); registerReceiver(mDictionaryPackInstallReceiver, packageFilter); final IntentFilter newDictFilter = new IntentFilter(); newDictFilter.addAction(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION); ContextCompat.registerReceiver(this, mDictionaryPackInstallReceiver, newDictFilter, ContextCompat.RECEIVER_NOT_EXPORTED); final IntentFilter dictDumpFilter = new IntentFilter(); dictDumpFilter.addAction(DictionaryDumpBroadcastReceiver.DICTIONARY_DUMP_INTENT_ACTION); ContextCompat.registerReceiver(this, mDictionaryDumpBroadcastReceiver, dictDumpFilter, ContextCompat.RECEIVER_NOT_EXPORTED); final IntentFilter restartAfterUnlockFilter = new IntentFilter(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) restartAfterUnlockFilter.addAction(Intent.ACTION_USER_UNLOCKED); registerReceiver(mRestartAfterDeviceUnlockReceiver, restartAfterUnlockFilter); StatsUtils.onCreate(mSettings.getCurrent(), mRichImm); } private void loadSettings() { final Locale locale = mRichImm.getCurrentSubtypeLocale(); final EditorInfo editorInfo = getCurrentInputEditorInfo(); final InputAttributes inputAttributes = new InputAttributes( editorInfo, isFullscreenMode(), getPackageName()); mSettings.loadSettings(this, locale, inputAttributes); final SettingsValues currentSettingsValues = mSettings.getCurrent(); AudioAndHapticFeedbackManager.getInstance().onSettingsChanged(currentSettingsValues); // This method is called on startup and language switch, before the new layout has // been displayed. Opening dictionaries never affects responsivity as dictionaries are // asynchronously loaded. if (!mHandler.hasPendingReopenDictionaries()) { resetDictionaryFacilitatorIfNecessary(); } refreshPersonalizationDictionarySession(currentSettingsValues); mInputLogic.mSuggest.clearNextWordSuggestionsCache(); mStatsUtilsManager.onLoadSettings(this, currentSettingsValues); } private void refreshPersonalizationDictionarySession( final SettingsValues currentSettingsValues) { if (!currentSettingsValues.mUsePersonalizedDicts) { // Remove user history dictionaries. PersonalizationHelper.removeAllUserHistoryDictionaries(this); mDictionaryFacilitator.clearUserHistoryDictionary(this); } } // Note that this method is called from a non-UI thread. @Override public void onUpdateMainDictionaryAvailability(final boolean isMainDictionaryAvailable) { final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView != null) { mainKeyboardView.setMainDictionaryAvailability(isMainDictionaryAvailable); } if (mHandler.hasPendingWaitForDictionaryLoad()) { mHandler.cancelWaitForDictionaryLoad(); mHandler.postResumeSuggestions(false /* shouldDelay */); } } void resetDictionaryFacilitatorIfNecessary() { final Locale subtypeSwitcherLocale = mRichImm.getCurrentSubtypeLocale(); final Locale subtypeLocale; if (subtypeSwitcherLocale == null) { // This happens in very rare corner cases - for example, immediately after a switch // to LatinIME has been requested, about a frame later another switch happens. In this // case, we are about to go down but we still don't know it, however the system tells // us there is no current subtype. Log.e(TAG, "System is reporting no current subtype."); subtypeLocale = ConfigurationCompatKt.locale(getResources().getConfiguration()); } else { subtypeLocale = subtypeSwitcherLocale; } if (mDictionaryFacilitator.isForLocale(subtypeLocale) && mDictionaryFacilitator.isForAccount(mSettings.getCurrent().mAccount) && mDictionaryFacilitator.usesContacts() == mSettings.getCurrent().mUseContactsDictionary && mDictionaryFacilitator.usesPersonalization() == mSettings.getCurrent().mUsePersonalizedDicts ) { return; } resetDictionaryFacilitator(subtypeLocale); } /** * Reset the facilitator by loading dictionaries for the given locale and * the current settings values. * * @param locale the locale */ // TODO: make sure the current settings always have the right locales, and read from them. private void resetDictionaryFacilitator(@NonNull final Locale locale) { final SettingsValues settingsValues = mSettings.getCurrent(); mDictionaryFacilitator.resetDictionaries(this, locale, settingsValues.mUseContactsDictionary, settingsValues.mUsePersonalizedDicts, false, settingsValues.mAccount, "", this); if (settingsValues.mAutoCorrectEnabled) { mInputLogic.mSuggest.setAutoCorrectionThreshold(settingsValues.mAutoCorrectionThreshold); } } /** * Reset suggest by loading the main dictionary of the current locale. */ /* package private */ void resetSuggestMainDict() { final SettingsValues settingsValues = mSettings.getCurrent(); mDictionaryFacilitator.resetDictionaries(this /* context */, mDictionaryFacilitator.getMainLocale(), settingsValues.mUseContactsDictionary, settingsValues.mUsePersonalizedDicts, true /* forceReloadMainDictionary */, settingsValues.mAccount, "" /* dictNamePrefix */, this /* DictionaryInitializationListener */); } // used for debug public String getLocaleAndConfidenceInfo() { return mDictionaryFacilitator.localesAndConfidences(); } @Override public void onDestroy() { mClipboardHistoryManager.onDestroy(); mDictionaryFacilitator.closeDictionaries(); mSettings.onDestroy(); unregisterReceiver(mRingerModeChangeReceiver); unregisterReceiver(mDictionaryPackInstallReceiver); unregisterReceiver(mDictionaryDumpBroadcastReceiver); unregisterReceiver(mRestartAfterDeviceUnlockReceiver); mStatsUtilsManager.onDestroy(this /* context */); super.onDestroy(); mHandler.removeCallbacksAndMessages(null); deallocateMemory(); } public void recycle() { unregisterReceiver(mDictionaryPackInstallReceiver); unregisterReceiver(mDictionaryDumpBroadcastReceiver); unregisterReceiver(mRingerModeChangeReceiver); unregisterReceiver(mRestartAfterDeviceUnlockReceiver); mInputLogic.recycle(); } private boolean isImeSuppressedByHardwareKeyboard() { final KeyboardSwitcher switcher = KeyboardSwitcher.getInstance(); return !onEvaluateInputViewShown() && switcher.isImeSuppressedByHardwareKeyboard( mSettings.getCurrent(), switcher.getKeyboardSwitchState()); } @Override public void onConfigurationChanged(final Configuration conf) { SettingsValues settingsValues = mSettings.getCurrent(); Log.i(TAG, "onConfigurationChanged"); SubtypeSettingsKt.reloadSystemLocales(this); if (settingsValues.mDisplayOrientation != conf.orientation) { mHandler.startOrientationChanging(); mInputLogic.onOrientationChange(mSettings.getCurrent()); } if (settingsValues.mHasHardwareKeyboard != Settings.readHasHardwareKeyboard(conf)) { // If the state of having a hardware keyboard changed, then we want to reload the // settings to adjust for that. // TODO: we should probably do this unconditionally here, rather than only when we // have a change in hardware keyboard configuration. loadSettings(); if (isImeSuppressedByHardwareKeyboard()) { // We call cleanupInternalStateForFinishInput() because it's the right thing to do; // however, it seems at the moment the framework is passing us a seemingly valid // but actually non-functional InputConnection object. So if this bug ever gets // fixed we'll be able to remove the composition, but until it is this code is // actually not doing much. cleanupInternalStateForFinishInput(); } } // KeyboardSwitcher will check by itself if theme update is necessary mKeyboardSwitcher.updateKeyboardTheme(getDisplayContext()); super.onConfigurationChanged(conf); } @Override public void onInitializeInterface() { mDisplayContext = getDisplayContext(); Log.d(TAG, "onInitializeInterface"); mKeyboardSwitcher.updateKeyboardTheme(mDisplayContext); } /** * Returns the context object whose resources are adjusted to match the metrics of the display. *

* Note that before {@link android.os.Build.VERSION_CODES#KITKAT}, there is no way to support * multi-display scenarios, so the context object will just return the IME context itself. *

* With initiating multi-display APIs from {@link android.os.Build.VERSION_CODES#KITKAT}, the * context object has to return with re-creating the display context according the metrics * of the display in runtime. *

* Starts from {@link android.os.Build.VERSION_CODES#S_V2}, the returning context object has * became to IME context self since it ends up capable of updating its resources internally. */ @SuppressWarnings("deprecation") private @NonNull Context getDisplayContext() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2) { // IME context sources is now managed by WindowProviderService from Android 12L. return this; } // An issue in Q that non-activity components Resources / DisplayMetrics in // Context doesn't well updated when the IME window moving to external display. // Currently we do a workaround is to create new display context directly and re-init // keyboard layout with this context. final WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE); return createDisplayContext(wm.getDefaultDisplay()); } @Override public View onCreateInputView() { StatsUtils.onCreateInputView(); return mKeyboardSwitcher.onCreateInputView(getDisplayContext(), mIsHardwareAcceleratedDrawingEnabled); } @Override public void setInputView(final View view) { super.setInputView(view); mInputView = view; mInsetsUpdater = ViewOutlineProviderUtilsKt.setInsetsOutlineProvider(view); updateSoftInputWindowLayoutParameters(); mSuggestionStripView = view.findViewById(R.id.suggestion_strip_view); if (hasSuggestionStripView()) { mSuggestionStripView.setListener(this, view); } } @Override public void setCandidatesView(final View view) { // To ensure that CandidatesView will never be set. } @Override public void onStartInput(final EditorInfo editorInfo, final boolean restarting) { mHandler.onStartInput(editorInfo, restarting); } @Override public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) { mHandler.onStartInputView(editorInfo, restarting); mStatsUtilsManager.onStartInputView(); } @Override public void onFinishInputView(final boolean finishingInput) { StatsUtils.onFinishInputView(); mHandler.onFinishInputView(finishingInput); mStatsUtilsManager.onFinishInputView(); mGestureConsumer = GestureConsumer.NULL_GESTURE_CONSUMER; } @Override public void onFinishInput() { mHandler.onFinishInput(); } @Override public void onCurrentInputMethodSubtypeChanged(final InputMethodSubtype subtype) { // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged() // is not guaranteed. It may even be called at the same time on a different thread. InputMethodSubtype oldSubtype = mRichImm.getCurrentSubtype().getRawSubtype(); StatsUtils.onSubtypeChanged(oldSubtype, subtype); mRichImm.onSubtypeChanged(subtype); mInputLogic.onSubtypeChanged(SubtypeLocaleUtils.getCombiningRulesExtraValue(subtype), mSettings.getCurrent()); loadKeyboard(); if (mSuggestionStripView != null) mSuggestionStripView.setRtl(mRichImm.getCurrentSubtype().isRtlSubtype()); } /** alias to onCurrentInputMethodSubtypeChanged with a better name, as it's also used for internal switching */ public void switchToSubtype(final InputMethodSubtype subtype) { onCurrentInputMethodSubtypeChanged(subtype); } void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) { super.onStartInput(editorInfo, restarting); final List hintLocales = EditorInfoCompatUtils.getHintLocales(editorInfo); if (hintLocales == null) { return; } // Try switching to a subtype matching the hint language. for (final Locale hintLocale : hintLocales) { if (LocaleUtils.INSTANCE.getMatchLevel(hintLocale, mRichImm.getCurrentSubtypeLocale()) >= 3) return; // current locale is already a good match, and we want to avoid unnecessary layout switches final InputMethodSubtype newSubtype = mRichImm.findSubtypeForHintLocale(hintLocale); if (newSubtype == null) continue; if (newSubtype.equals(mRichImm.getCurrentSubtype().getRawSubtype())) return; // no need to switch, we already use the correct locale mHandler.postSwitchLanguage(newSubtype); break; } } void onStartInputViewInternal(final EditorInfo editorInfo, final boolean restarting) { super.onStartInputView(editorInfo, restarting); mDictionaryFacilitator.onStartInput(); // Switch to the null consumer to handle cases leading to early exit below, for which we // also wouldn't be consuming gesture data. mGestureConsumer = GestureConsumer.NULL_GESTURE_CONSUMER; mRichImm.refreshSubtypeCaches(); final KeyboardSwitcher switcher = mKeyboardSwitcher; switcher.updateKeyboardTheme(mDisplayContext); final MainKeyboardView mainKeyboardView = switcher.getMainKeyboardView(); // If we are starting input in a different text field from before, we'll have to reload // settings, so currentSettingsValues can't be final. SettingsValues currentSettingsValues = mSettings.getCurrent(); if (editorInfo == null) { Log.e(TAG, "Null EditorInfo in onStartInputView()"); if (DebugFlags.DEBUG_ENABLED) { throw new NullPointerException("Null EditorInfo in onStartInputView()"); } return; } if (DebugFlags.DEBUG_ENABLED) { Log.d(TAG, "onStartInputView: editorInfo:" + String.format("inputType=0x%08x imeOptions=0x%08x", editorInfo.inputType, editorInfo.imeOptions)); Log.d(TAG, "All caps = " + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) + ", sentence caps = " + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) + ", word caps = " + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0)); } Log.i(TAG, "Starting input. Cursor position = " + editorInfo.initialSelStart + "," + editorInfo.initialSelEnd); // In landscape mode, this method gets called without the input view being created. if (mainKeyboardView == null) { return; } // Update to a gesture consumer with the current editor and IME state. mGestureConsumer = GestureConsumer.newInstance(editorInfo, mInputLogic.getPrivateCommandPerformer(), mRichImm.getCurrentSubtypeLocale(), switcher.getKeyboard()); // Forward this event to the accessibility utilities, if enabled. final AccessibilityUtils accessUtils = AccessibilityUtils.Companion.getInstance(); if (accessUtils.isTouchExplorationEnabled()) { accessUtils.onStartInputViewInternal(mainKeyboardView, editorInfo, restarting); } final boolean inputTypeChanged = !currentSettingsValues.isSameInputType(editorInfo); final boolean isDifferentTextField = !restarting || inputTypeChanged; StatsUtils.onStartInputView(editorInfo.inputType, Settings.getInstance().getCurrent().mDisplayOrientation, !isDifferentTextField); // The EditorInfo might have a flag that affects fullscreen mode. // Note: This call should be done by InputMethodService? updateFullscreenMode(); // ALERT: settings have not been reloaded and there is a chance they may be stale. // In the practice, if it is, we should have gotten onConfigurationChanged so it should // be fine, but this is horribly confusing and must be fixed AS SOON AS POSSIBLE. // In some cases the input connection has not been reset yet and we can't access it. In // this case we will need to call loadKeyboard() later, when it's accessible, so that we // can go into the correct mode, so we need to do some housekeeping here. final boolean needToCallLoadKeyboardLater; final Suggest suggest = mInputLogic.mSuggest; if (!isImeSuppressedByHardwareKeyboard()) { // The app calling setText() has the effect of clearing the composing // span, so we should reset our state unconditionally, even if restarting is true. // We also tell the input logic about the combining rules for the current subtype, so // it can adjust its combiners if needed. mInputLogic.startInput(mRichImm.getCombiningRulesExtraValueOfCurrentSubtype(), currentSettingsValues); resetDictionaryFacilitatorIfNecessary(); // TODO[IL]: Can the following be moved to InputLogic#startInput? if (!mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess( editorInfo.initialSelStart, editorInfo.initialSelEnd, false /* shouldFinishComposition */)) { // Sometimes, while rotating, for some reason the framework tells the app we are not // connected to it and that means we can't refresh the cache. In this case, schedule // a refresh later. // We try resetting the caches up to 5 times before giving up. mHandler.postResetCaches(isDifferentTextField, 5 /* remainingTries */); // mLastSelection{Start,End} are reset later in this method, no need to do it here needToCallLoadKeyboardLater = true; } else { // When rotating, and when input is starting again in a field from where the focus // didn't move (the keyboard having been closed with the back key), // initialSelStart and initialSelEnd sometimes are lying. Make a best effort to // work around this bug. mInputLogic.mConnection.tryFixLyingCursorPosition(); mHandler.postResumeSuggestions(true /* shouldDelay */); needToCallLoadKeyboardLater = false; } } else { // If we have a hardware keyboard we don't need to call loadKeyboard later anyway. needToCallLoadKeyboardLater = false; } if (isDifferentTextField || !currentSettingsValues.hasSameOrientation(getResources().getConfiguration())) { loadSettings(); } if (isDifferentTextField) { mainKeyboardView.closing(); currentSettingsValues = mSettings.getCurrent(); if (currentSettingsValues.mAutoCorrectEnabled) { suggest.setAutoCorrectionThreshold(currentSettingsValues.mAutoCorrectionThreshold); } switcher.loadKeyboard(editorInfo, currentSettingsValues, getCurrentAutoCapsState(), getCurrentRecapitalizeState()); if (needToCallLoadKeyboardLater) { // If we need to call loadKeyboard again later, we need to save its state now. The // later call will be done in #retryResetCaches. switcher.saveKeyboardState(); } } else if (restarting) { // TODO: Come up with a more comprehensive way to reset the keyboard layout when // a keyboard layout set doesn't get reloaded in this method. switcher.resetKeyboardStateToAlphabet(getCurrentAutoCapsState(), getCurrentRecapitalizeState()); // In apps like Talk, we come here when the text is sent and the field gets emptied and // we need to re-evaluate the shift state, but not the whole layout which would be // disruptive. // Space state must be updated before calling updateShiftState switcher.requestUpdatingShiftState(getCurrentAutoCapsState(), getCurrentRecapitalizeState()); } // This will set the punctuation suggestions if next word suggestion is off; // otherwise it will clear the suggestion strip. setNeutralSuggestionStrip(); mHandler.cancelUpdateSuggestionStrip(); mainKeyboardView.setMainDictionaryAvailability(mDictionaryFacilitator.hasAtLeastOneInitializedMainDictionary()); mainKeyboardView.setKeyPreviewPopupEnabled(currentSettingsValues.mKeyPreviewPopupOn); mainKeyboardView.setSlidingKeyInputPreviewEnabled(currentSettingsValues.mSlidingKeyInputPreviewEnabled); mainKeyboardView.setGestureHandlingEnabledByUser( currentSettingsValues.mGestureInputEnabled, currentSettingsValues.mGestureTrailEnabled, currentSettingsValues.mGestureFloatingPreviewTextEnabled); if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); } @Override public void onWindowShown() { super.onWindowShown(); if (isInputViewShown()) { setNavigationBarColor(); workaroundForHuaweiStatusBarIssue(); } } @Override public void onWindowHidden() { super.onWindowHidden(); final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView != null) { mainKeyboardView.closing(); } clearNavigationBarColor(); } void onFinishInputInternal() { super.onFinishInput(); Log.i(TAG, "onFinishInput"); mDictionaryFacilitator.onFinishInput(this); final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView != null) { mainKeyboardView.closing(); } } void onFinishInputViewInternal(final boolean finishingInput) { super.onFinishInputView(finishingInput); Log.i(TAG, "onFinishInputView"); cleanupInternalStateForFinishInput(); } private void cleanupInternalStateForFinishInput() { // Remove pending messages related to update suggestions mHandler.cancelUpdateSuggestionStrip(); // Should do the following in onFinishInputInternal but until JB MR2 it's not called :( mInputLogic.finishInput(); mKeyboardActionListener.resetMetaState(); } protected void deallocateMemory() { mKeyboardSwitcher.deallocateMemory(); } @Override public void onUpdateSelection(final int oldSelStart, final int oldSelEnd, final int newSelStart, final int newSelEnd, final int composingSpanStart, final int composingSpanEnd) { super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart, composingSpanEnd); if (DebugFlags.DEBUG_ENABLED) { Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart + ", ose=" + oldSelEnd + ", nss=" + newSelStart + ", nse=" + newSelEnd + ", cs=" + composingSpanStart + ", ce=" + composingSpanEnd); } // This call happens whether our view is displayed or not, but if it's not then we should // not attempt recorrection. This is true even with a hardware keyboard connected: if the // view is not displayed we have no means of showing suggestions anyway, and if it is then // we want to show suggestions anyway. final SettingsValues settingsValues = mSettings.getCurrent(); if (isInputViewShown() && mInputLogic.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, settingsValues)) { mKeyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(), getCurrentRecapitalizeState()); } } /** * This is called when the user has clicked on the extracted text view, * when running in fullscreen mode. The default implementation hides * the suggestions view when this happens, but only if the extracted text * editor has a vertical scroll bar because its text doesn't fit. * Here we override the behavior due to the possibility that a re-correction could * cause the suggestions strip to disappear and re-appear. */ @Override public void onExtractedTextClicked() { if (mSettings.getCurrent().needsToLookupSuggestions()) { return; } super.onExtractedTextClicked(); } /** * This is called when the user has performed a cursor movement in the * extracted text view, when it is running in fullscreen mode. The default * implementation hides the suggestions view when a vertical movement * happens, but only if the extracted text editor has a vertical scroll bar * because its text doesn't fit. * Here we override the behavior due to the possibility that a re-correction could * cause the suggestions strip to disappear and re-appear. */ @Override public void onExtractedCursorMovement(final int dx, final int dy) { if (mSettings.getCurrent().needsToLookupSuggestions()) { return; } super.onExtractedCursorMovement(dx, dy); } @Override public void hideWindow() { if (mSuggestionStripView != null) mSuggestionStripView.setToolbarVisibility(false); mKeyboardSwitcher.onHideWindow(); if (TRACE) Debug.stopMethodTracing(); if (isShowingOptionDialog()) { mOptionsDialog.dismiss(); mOptionsDialog = null; } super.hideWindow(); } @Override public void onDisplayCompletions(final CompletionInfo[] applicationSpecifiedCompletions) { if (DebugFlags.DEBUG_ENABLED) { Log.i(TAG, "Received completions:"); if (applicationSpecifiedCompletions != null) { for (int i = 0; i < applicationSpecifiedCompletions.length; i++) { Log.i(TAG, " #" + i + ": " + applicationSpecifiedCompletions[i]); } } } if (!mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()) { return; } // If we have an update request in flight, we need to cancel it so it does not override // these completions. mHandler.cancelUpdateSuggestionStrip(); if (applicationSpecifiedCompletions == null) { setNeutralSuggestionStrip(); return; } final ArrayList applicationSuggestedWords = SuggestedWords.getFromApplicationSpecifiedCompletions( applicationSpecifiedCompletions); final SuggestedWords suggestedWords = new SuggestedWords(applicationSuggestedWords, null /* rawSuggestions */, null /* typedWord */, false /* typedWordValid */, false /* willAutoCorrect */, false /* isObsoleteSuggestions */, SuggestedWords.INPUT_STYLE_APPLICATION_SPECIFIED /* inputStyle */, SuggestedWords.NOT_A_SEQUENCE_NUMBER); // When in fullscreen mode, show completions generated by the application forcibly setSuggestedWords(suggestedWords); } @Override public void onComputeInsets(final InputMethodService.Insets outInsets) { super.onComputeInsets(outInsets); // This method may be called before {@link #setInputView(View)}. if (mInputView == null) { return; } final View visibleKeyboardView = mKeyboardSwitcher.getWrapperView(); if (visibleKeyboardView == null || !hasSuggestionStripView()) { return; } final int inputHeight = mInputView.getHeight(); if (isImeSuppressedByHardwareKeyboard() && !visibleKeyboardView.isShown()) { // If there is a hardware keyboard and a visible software keyboard view has been hidden, // no visual element will be shown on the screen. outInsets.contentTopInsets = inputHeight; outInsets.visibleTopInsets = inputHeight; mInsetsUpdater.setInsets(outInsets); return; } final int visibleTopY = inputHeight - visibleKeyboardView.getHeight() - mSuggestionStripView.getHeight(); mSuggestionStripView.setMoreSuggestionsHeight(visibleTopY); // Need to set expanded touchable region only if a keyboard view is being shown. if (visibleKeyboardView.isShown()) { final int touchLeft = 0; final int touchTop = mKeyboardSwitcher.isShowingPopupKeysPanel() ? 0 : visibleTopY; final int touchRight = visibleKeyboardView.getWidth(); final int touchBottom = inputHeight // Extend touchable region below the keyboard. + EXTENDED_TOUCHABLE_REGION_HEIGHT; outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION; outInsets.touchableRegion.set(touchLeft, touchTop, touchRight, touchBottom); } outInsets.contentTopInsets = visibleTopY; outInsets.visibleTopInsets = visibleTopY; mInsetsUpdater.setInsets(outInsets); } public void startShowingInputView(final boolean needsToLoadKeyboard) { mIsExecutingStartShowingInputView = true; // This {@link #showWindow(boolean)} will eventually call back // {@link #onEvaluateInputViewShown()}. showWindow(true /* showInput */); mIsExecutingStartShowingInputView = false; if (needsToLoadKeyboard) { loadKeyboard(); } } public void stopShowingInputView() { showWindow(false /* showInput */); } @Override public boolean onShowInputRequested(final int flags, final boolean configChange) { if (isImeSuppressedByHardwareKeyboard()) { return true; } return super.onShowInputRequested(flags, configChange); } @Override public boolean onEvaluateInputViewShown() { if (mIsExecutingStartShowingInputView) { return true; } return super.onEvaluateInputViewShown(); } @Override public boolean onEvaluateFullscreenMode() { if (isImeSuppressedByHardwareKeyboard()) { // If there is a hardware keyboard, disable full screen mode. return false; } // Reread resource value here, because this method is called by the framework as needed. final boolean isFullscreenModeAllowed = Settings.readFullscreenModeAllowed(getResources()); if (super.onEvaluateFullscreenMode() && isFullscreenModeAllowed) { // TODO: Remove this hack. Actually we should not really assume NO_EXTRACT_UI // implies NO_FULLSCREEN. However, the framework mistakenly does. i.e. NO_EXTRACT_UI // without NO_FULLSCREEN doesn't work as expected. Because of this we need this // hack for now. Let's get rid of this once the framework gets fixed. final EditorInfo ei = getCurrentInputEditorInfo(); if (ei == null) return false; final boolean noExtractUi = (ei.imeOptions & EditorInfo.IME_FLAG_NO_EXTRACT_UI) != 0; final boolean noFullscreen = (ei.imeOptions & EditorInfo.IME_FLAG_NO_FULLSCREEN) != 0; if (noExtractUi || noFullscreen) return false; if (mKeyboardSwitcher.getVisibleKeyboardView() == null || mSuggestionStripView == null) return false; final int usedHeight = mKeyboardSwitcher.getVisibleKeyboardView().getHeight() + mSuggestionStripView.getHeight(); final int availableHeight = getResources().getDisplayMetrics().heightPixels; return usedHeight > availableHeight * 0.6; // if we have less than 40% available, use fullscreen mode } return false; } @Override public void updateFullscreenMode() { super.updateFullscreenMode(); updateSoftInputWindowLayoutParameters(); } @Override @RequiresApi(api = Build.VERSION_CODES.R) public InlineSuggestionsRequest onCreateInlineSuggestionsRequest(@NonNull Bundle uiExtras) { Log.d(TAG,"onCreateInlineSuggestionsRequest called"); return InlineAutofillUtils.createInlineSuggestionRequest(mDisplayContext); } @Override @RequiresApi(api = Build.VERSION_CODES.R) public boolean onInlineSuggestionsResponse(InlineSuggestionsResponse response) { Log.d(TAG,"onInlineSuggestionsResponse called"); final List inlineSuggestions = response.getInlineSuggestions(); if (inlineSuggestions.isEmpty()) { return false; } final View inlineSuggestionView = InlineAutofillUtils.createView(inlineSuggestions, mDisplayContext); // Without this function the inline autofill suggestions will not be visible mHandler.cancelResumeSuggestions(); mSuggestionStripView.setInlineSuggestionsView(inlineSuggestionView); return true; } private void updateSoftInputWindowLayoutParameters() { // Override layout parameters to expand {@link SoftInputWindow} to the entire screen. // See {@link InputMethodService#setinputView(View)} and // {@link SoftInputWindow#updateWidthHeight(WindowManager.LayoutParams)}. final Window window = getWindow().getWindow(); if (window == null) return; ViewLayoutUtils.updateLayoutHeightOf(window, LayoutParams.MATCH_PARENT); // This method may be called before {@link #setInputView(View)}. if (mInputView != null) { // In non-fullscreen mode, {@link InputView} and its parent inputArea should expand to // the entire screen and be placed at the bottom of {@link SoftInputWindow}. // In fullscreen mode, these shouldn't expand to the entire screen and should be // coexistent with {@link #mExtractedArea} above. // See {@link InputMethodService#setInputView(View) and // com.android.internal.R.layout.input_method.xml. final int layoutHeight = isFullscreenMode() ? LayoutParams.WRAP_CONTENT : LayoutParams.MATCH_PARENT; final View inputArea = window.findViewById(android.R.id.inputArea); ViewLayoutUtils.updateLayoutHeightOf(inputArea, layoutHeight); ViewLayoutUtils.updateLayoutGravityOf(inputArea, Gravity.BOTTOM); ViewLayoutUtils.updateLayoutHeightOf(mInputView, layoutHeight); } } public int getCurrentAutoCapsState() { return mInputLogic.getCurrentAutoCapsState(mSettings.getCurrent()); } public int getCurrentRecapitalizeState() { return mInputLogic.getCurrentRecapitalizeState(); } /** * @param codePoints code points to get coordinates for. * @return x,y coordinates for this keyboard, as a flattened array. */ public int[] getCoordinatesForCurrentKeyboard(final int[] codePoints) { final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); if (null == keyboard) { return CoordinateUtils.newCoordinateArray(codePoints.length, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); } return keyboard.getCoordinates(codePoints); } @Override public void onRequestPermissionsResult(boolean allGranted) { setNeutralSuggestionStrip(); } public void displaySettingsDialog() { launchSettings(); } public boolean showInputPickerDialog() { if (isShowingOptionDialog()) return false; if (mRichImm.hasMultipleEnabledIMEsOrSubtypes(true)) { mOptionsDialog = InputMethodPickerKt.createInputMethodPickerDialog(this, mRichImm, mKeyboardSwitcher.getMainKeyboardView().getWindowToken()); mOptionsDialog.show(); return true; } return false; } private boolean isShowingOptionDialog() { return mOptionsDialog != null && mOptionsDialog.isShowing(); } // called when language switch key is pressed (either the keyboard key, or long-press comma) public void switchToNextSubtype() { final boolean switchSubtype = mSettings.getCurrent().mLanguageSwitchKeyToOtherSubtypes; final boolean switchIme = mSettings.getCurrent().mLanguageSwitchKeyToOtherImes; // switch IME if wanted and possible if (switchIme && !switchSubtype && switchInputMethod()) return; final boolean hasMoreThanOneSubtype = mRichImm.getMyEnabledInputMethodSubtypeList(false).size() > 1; // switch subtype if wanted and possible if (switchSubtype && !switchIme && hasMoreThanOneSubtype) { // switch to previous subtype if current one was used, otherwise cycle through list mSubtypeState.switchSubtype(mRichImm); return; } // language key set to switch both, or language key is not shown on keyboard -> switch both if (hasMoreThanOneSubtype && mSubtypeState.mCurrentSubtypeHasBeenUsed) { mSubtypeState.switchSubtype(mRichImm); return; } if (shouldSwitchToOtherInputMethods()) { final InputMethodSubtype nextSubtype = mRichImm.getNextSubtypeInThisIme(false); if (nextSubtype != null) { switchToSubtype(nextSubtype); return; } else if (switchInputMethod()) { return; } } mSubtypeState.switchSubtype(mRichImm); } @SuppressWarnings("deprecation") private boolean switchInputMethod() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) return switchToNextInputMethod(false); final Window window = getWindow().getWindow(); if (window == null) return false; final IBinder token = window.getAttributes().token; return mRichImm.getInputMethodManager().switchToNextInputMethod(token, false); } @SuppressWarnings("deprecation") public boolean shouldSwitchToOtherInputMethods() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) return shouldOfferSwitchingToNextInputMethod(); final Window window = getWindow().getWindow(); if (window == null) return mSettings.getCurrent().mLanguageSwitchKeyToOtherImes; final IBinder token = window.getAttributes().token; if (token == null) { return mSettings.getCurrent().mLanguageSwitchKeyToOtherImes; } return mRichImm.getInputMethodManager().shouldOfferSwitchingToNextInputMethod(token); } public void switchInputMethodAndSubtype(final InputMethodInfo imi, final InputMethodSubtype subtype) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { switchInputMethod(imi.getId(), subtype); } else { final Window window = getWindow().getWindow(); if (window == null) return; final IBinder token = window.getAttributes().token; mRichImm.getInputMethodManager().setInputMethodAndSubtype(token, imi.getId(), subtype); } } // Implementation of {@link SuggestionStripView.Listener}. @Override public void onCodeInput(final int codePoint, final int x, final int y, final boolean isKeyRepeat) { onCodeInput(codePoint, 0, x, y, isKeyRepeat); } public void onCodeInput(final int codePoint, final int metaState, final int x, final int y, final boolean isKeyRepeat) { if (codePoint < 0) { switch (codePoint) { case KeyCode.TOGGLE_AUTOCORRECT -> {mSettings.toggleAutoCorrect(); return; } case KeyCode.TOGGLE_INCOGNITO_MODE -> {mSettings.toggleAlwaysIncognitoMode(); return; } } } // TODO: this processing does not belong inside LatinIME, the caller should be doing this. final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); // x and y include some padding, but everything down the line (especially native // code) needs the coordinates in the keyboard frame. // TODO: We should reconsider which coordinate system should be used to represent // keyboard event. Also we should pull this up -- LatinIME has no business doing // this transformation, it should be done already before calling onEvent. final int keyX = mainKeyboardView.getKeyX(x); final int keyY = mainKeyboardView.getKeyY(y); final Event event = createSoftwareKeypressEvent(codePoint, metaState, keyX, keyY, isKeyRepeat); onEvent(event); } // This method is public for testability of LatinIME, but also in the future it should // completely replace #onCodeInput. public void onEvent(@NonNull final Event event) { if (KeyCode.VOICE_INPUT == event.getMKeyCode()) { mRichImm.switchToShortcutIme(this); } final InputTransaction completeInputTransaction = mInputLogic.onCodeInput(mSettings.getCurrent(), event, mKeyboardSwitcher.getKeyboardShiftMode(), mKeyboardSwitcher.getCurrentKeyboardScript(), mHandler); updateStateAfterInputTransaction(completeInputTransaction); mKeyboardSwitcher.onEvent(event, getCurrentAutoCapsState(), getCurrentRecapitalizeState()); } // A helper method to split the code point and the key code. Ultimately, they should not be // squashed into the same variable, and this method should be removed. // public for testing, as we don't want to copy the same logic into test code @NonNull public static Event createSoftwareKeypressEvent(final int keyCodeOrCodePoint, final int metaState, final int keyX, final int keyY, final boolean isKeyRepeat) { final int keyCode; final int codePoint; if (keyCodeOrCodePoint <= 0) { keyCode = keyCodeOrCodePoint; codePoint = Event.NOT_A_CODE_POINT; } else { keyCode = Event.NOT_A_KEY_CODE; codePoint = keyCodeOrCodePoint; } return Event.createSoftwareKeypressEvent(codePoint, keyCode, metaState, keyX, keyY, isKeyRepeat); } public void onTextInput(final String rawText) { // TODO: have the keyboard pass the correct key code when we need it. final Event event = Event.createSoftwareTextEvent(rawText, KeyCode.MULTIPLE_CODE_POINTS); final InputTransaction completeInputTransaction = mInputLogic.onTextInput(mSettings.getCurrent(), event, mKeyboardSwitcher.getKeyboardShiftMode(), mHandler); updateStateAfterInputTransaction(completeInputTransaction); mInputLogic.restartSuggestionsOnWordTouchedByCursor(mSettings.getCurrent(), mKeyboardSwitcher.getCurrentKeyboardScript()); mKeyboardSwitcher.onEvent(event, getCurrentAutoCapsState(), getCurrentRecapitalizeState()); } public void onStartBatchInput() { mInputLogic.onStartBatchInput(mSettings.getCurrent(), mKeyboardSwitcher, mHandler); mGestureConsumer.onGestureStarted(mRichImm.getCurrentSubtypeLocale(), mKeyboardSwitcher.getKeyboard()); } public void onUpdateBatchInput(final InputPointers batchPointers) { mInputLogic.onUpdateBatchInput(batchPointers); } public void onEndBatchInput(final InputPointers batchPointers) { mInputLogic.onEndBatchInput(batchPointers); mGestureConsumer.onGestureCompleted(batchPointers); } public void onCancelBatchInput() { mInputLogic.onCancelBatchInput(mHandler); mGestureConsumer.onGestureCanceled(); } /** * To be called after the InputLogic has gotten a chance to act on the suggested words by the * IME for the full gesture, possibly updating the TextView to reflect the first suggestion. *

* This method must be run on the UI Thread. * @param suggestedWords suggested words by the IME for the full gesture. */ public void onTailBatchInputResultShown(final SuggestedWords suggestedWords) { mGestureConsumer.onImeSuggestionsProcessed(suggestedWords, mInputLogic.getComposingStart(), mInputLogic.getComposingLength(), mDictionaryFacilitator); } // This method must run on the UI Thread. void showGesturePreviewAndSuggestionStrip(@NonNull final SuggestedWords suggestedWords, final boolean dismissGestureFloatingPreviewText) { showSuggestionStrip(suggestedWords); final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); mainKeyboardView.showGestureFloatingPreviewText(suggestedWords, dismissGestureFloatingPreviewText /* dismissDelayed */); } public boolean hasSuggestionStripView() { return null != mSuggestionStripView; } private void setSuggestedWords(final SuggestedWords suggestedWords) { final SettingsValues currentSettingsValues = mSettings.getCurrent(); mInputLogic.setSuggestedWords(suggestedWords); // TODO: Modify this when we support suggestions with hard keyboard if (!hasSuggestionStripView()) { return; } if (!onEvaluateInputViewShown()) { return; } final boolean isEmptyApplicationSpecifiedCompletions = currentSettingsValues.isApplicationSpecifiedCompletionsOn() && suggestedWords.isEmpty(); final boolean noSuggestionsFromDictionaries = suggestedWords.isEmpty() || suggestedWords.isPunctuationSuggestions() || isEmptyApplicationSpecifiedCompletions; if (currentSettingsValues.isSuggestionsEnabledPerUserSettings() || currentSettingsValues.isApplicationSpecifiedCompletionsOn() // We should clear the contextual strip if there is no suggestion from dictionaries. || noSuggestionsFromDictionaries) { mSuggestionStripView.setSuggestions(suggestedWords, mRichImm.getCurrentSubtype().isRtlSubtype()); } } // TODO[IL]: Move this out of LatinIME. public void getSuggestedWords(final int inputStyle, final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); if (keyboard == null) { callback.onGetSuggestedWords(SuggestedWords.getEmptyInstance()); return; } mInputLogic.getSuggestedWords(mSettings.getCurrent(), keyboard, mKeyboardSwitcher.getKeyboardShiftMode(), inputStyle, sequenceNumber, callback); } @Override public void showSuggestionStrip(final SuggestedWords suggestedWords) { if (suggestedWords.isEmpty()) { setNeutralSuggestionStrip(); } else { setSuggestedWords(suggestedWords); } // Cache the auto-correction in accessibility code so we can speak it if the user // touches a key that will insert it. AccessibilityUtils.Companion.getInstance().setAutoCorrection(suggestedWords); } // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener} // interface @Override public void pickSuggestionManually(final SuggestedWordInfo suggestionInfo) { final InputTransaction completeInputTransaction = mInputLogic.onPickSuggestionManually( mSettings.getCurrent(), suggestionInfo, mKeyboardSwitcher.getKeyboardShiftMode(), mKeyboardSwitcher.getCurrentKeyboardScript(), mHandler); updateStateAfterInputTransaction(completeInputTransaction); } // This will show either an empty suggestion strip (if prediction is enabled) or // punctuation suggestions (if it's disabled). @Override public void setNeutralSuggestionStrip() { final SettingsValues currentSettings = mSettings.getCurrent(); final SuggestedWords neutralSuggestions = currentSettings.mBigramPredictionEnabled ? SuggestedWords.getEmptyInstance() : currentSettings.mSpacingAndPunctuations.mSuggestPuncList; setSuggestedWords(neutralSuggestions); } @Override public void removeSuggestion(final String word) { mDictionaryFacilitator.removeWord(word); } private void loadKeyboard() { // Since we are switching languages, the most urgent thing is to let the keyboard graphics // update. LoadKeyboard does that, but we need to wait for buffer flip for it to be on // the screen. Anything we do right now will delay this, so wait until the next frame // before we do the rest, like reopening dictionaries and updating suggestions. So we // post a message. mHandler.postReopenDictionaries(); loadSettings(); if (mKeyboardSwitcher.getMainKeyboardView() != null) { // Reload keyboard because the current language has been changed. mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent(), getCurrentAutoCapsState(), getCurrentRecapitalizeState()); } } /** * After an input transaction has been executed, some state must be updated. This includes * the shift state of the keyboard and suggestions. This method looks at the finished * inputTransaction to find out what is necessary and updates the state accordingly. * @param inputTransaction The transaction that has been executed. */ private void updateStateAfterInputTransaction(final InputTransaction inputTransaction) { switch (inputTransaction.getRequiredShiftUpdate()) { case InputTransaction.SHIFT_UPDATE_LATER -> mHandler.postUpdateShiftState(); case InputTransaction.SHIFT_UPDATE_NOW -> mKeyboardSwitcher .requestUpdatingShiftState(getCurrentAutoCapsState(), getCurrentRecapitalizeState()); default -> { } // SHIFT_NO_UPDATE } if (inputTransaction.requiresUpdateSuggestions()) { final int inputStyle; if (inputTransaction.getMEvent().isSuggestionStripPress()) { // Suggestion strip press: no input. inputStyle = SuggestedWords.INPUT_STYLE_NONE; } else if (inputTransaction.getMEvent().isGesture()) { inputStyle = SuggestedWords.INPUT_STYLE_TAIL_BATCH; } else { inputStyle = SuggestedWords.INPUT_STYLE_TYPING; } mHandler.postUpdateSuggestionStrip(inputStyle); } if (inputTransaction.didAffectContents()) { mSubtypeState.setCurrentSubtypeHasBeenUsed(); } } public void hapticAndAudioFeedback(final int code, final int repeatCount) { final MainKeyboardView keyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (keyboardView != null && keyboardView.isInDraggingFinger()) { // No need to feedback while finger is dragging. return; } if (repeatCount > 0) { if (code == KeyCode.DELETE && !mInputLogic.mConnection.canDeleteCharacters()) { // No need to feedback when repeat delete key will have no effect. return; } // TODO: Use event time that the last feedback has been generated instead of relying on // a repeat count to thin out feedback. if (repeatCount % PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT == 0) { return; } } final AudioAndHapticFeedbackManager feedbackManager = AudioAndHapticFeedbackManager.getInstance(); if (repeatCount == 0) { // TODO: Reconsider how to perform haptic feedback when repeating key. feedbackManager.performHapticFeedback(keyboardView); } feedbackManager.performAudioFeedback(code); } private HardwareEventDecoder getHardwareKeyEventDecoder(final int deviceId) { final HardwareEventDecoder decoder = mHardwareEventDecoders.get(deviceId); if (null != decoder) return decoder; // TODO: create the decoder according to the specification final HardwareEventDecoder newDecoder = new HardwareKeyboardEventDecoder(deviceId); mHardwareEventDecoders.put(deviceId, newDecoder); return newDecoder; } // Hooks for hardware keyboard @Override public boolean onKeyDown(final int keyCode, final KeyEvent keyEvent) { if (mEmojiAltPhysicalKeyDetector == null) { mEmojiAltPhysicalKeyDetector = new EmojiAltPhysicalKeyDetector( getApplicationContext().getResources()); } mEmojiAltPhysicalKeyDetector.onKeyDown(keyEvent); if (!ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED) { return super.onKeyDown(keyCode, keyEvent); } final Event event; if (mRichImm.getCurrentSubtypeLocale().getLanguage().equals("ko")) { final RichInputMethodSubtype subtype = mKeyboardSwitcher.getKeyboard() == null ? mRichImm.getCurrentSubtype() : mKeyboardSwitcher.getKeyboard().mId.mSubtype; event = HangulEventDecoder.decodeHardwareKeyEvent(subtype, keyEvent, () -> getHardwareKeyEventDecoder(keyEvent.getDeviceId()).decodeHardwareKey(keyEvent)); } else { event = getHardwareKeyEventDecoder(keyEvent.getDeviceId()).decodeHardwareKey(keyEvent); } // If the event is not handled by LatinIME, we just pass it to the parent implementation. // If it's handled, we return true because we did handle it. if (event.isHandled()) { mInputLogic.onCodeInput(mSettings.getCurrent(), event, mKeyboardSwitcher.getKeyboardShiftMode(), // TODO: this is not necessarily correct for a hardware keyboard right now mKeyboardSwitcher.getCurrentKeyboardScript(), mHandler); return true; } return super.onKeyDown(keyCode, keyEvent); } @Override public boolean onKeyUp(final int keyCode, final KeyEvent keyEvent) { if (mEmojiAltPhysicalKeyDetector == null) { mEmojiAltPhysicalKeyDetector = new EmojiAltPhysicalKeyDetector( getApplicationContext().getResources()); } mEmojiAltPhysicalKeyDetector.onKeyUp(keyEvent); if (!ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED) { return super.onKeyUp(keyCode, keyEvent); } final long keyIdentifier = (long) keyEvent.getDeviceId() << 32 + keyEvent.getKeyCode(); if (mInputLogic.mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) { return true; } return super.onKeyUp(keyCode, keyEvent); } // onKeyDown and onKeyUp are the main events we are interested in. There are two more events // related to handling of hardware key events that we may want to implement in the future: // boolean onKeyLongPress(final int keyCode, final KeyEvent event); // boolean onKeyMultiple(final int keyCode, final int count, final KeyEvent event); // receive ringer mode change. private final BroadcastReceiver mRingerModeChangeReceiver = new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) { final String action = intent.getAction(); if (AudioManager.RINGER_MODE_CHANGED_ACTION.equals(action)) { AudioAndHapticFeedbackManager.getInstance().onRingerModeChanged(); } } }; public ClipboardHistoryManager getClipboardHistoryManager() { return mClipboardHistoryManager; } void launchSettings() { mInputLogic.commitTyped(mSettings.getCurrent(), LastComposedWord.NOT_A_SEPARATOR); requestHideSelf(0); final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView != null) { mainKeyboardView.closing(); } final Intent intent = new Intent(); intent.setClass(LatinIME.this, SettingsActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED | Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); } public void dumpDictionaryForDebug(final String dictName) { if (!mDictionaryFacilitator.isActive()) { resetDictionaryFacilitatorIfNecessary(); } mDictionaryFacilitator.dumpDictionaryForDebug(dictName); } public void debugDumpStateAndCrashWithException(final String context) { final SettingsValues settingsValues = mSettings.getCurrent(); String s = settingsValues.toString() + "\nAttributes : " + settingsValues.mInputAttributes + "\nContext : " + context; throw new RuntimeException(s); } @Override protected void dump(final FileDescriptor fd, final PrintWriter fout, final String[] args) { super.dump(fd, fout, args); final Printer p = new PrintWriterPrinter(fout); p.println("LatinIME state :"); p.println(" VersionCode = " + BuildConfig.VERSION_CODE); p.println(" VersionName = " + BuildConfig.VERSION_NAME); final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1; p.println(" Keyboard mode = " + keyboardMode); final SettingsValues settingsValues = mSettings.getCurrent(); p.println(settingsValues.dump()); p.println(mDictionaryFacilitator.dump(this)); } // slightly modified from Simple Keyboard: https://github.com/rkkr/simple-keyboard/blob/master/app/src/main/java/rkr/simplekeyboard/inputmethod/latin/LatinIME.java @SuppressWarnings("deprecation") private void setNavigationBarColor() { final SettingsValues settingsValues = mSettings.getCurrent(); if (!settingsValues.mCustomNavBarColor) return; final int color = settingsValues.mColors.get(ColorType.NAVIGATION_BAR); final Window window = getWindow().getWindow(); if (window == null) return; mOriginalNavBarColor = window.getNavigationBarColor(); window.setNavigationBarColor(color); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return; final View view = window.getDecorView(); mOriginalNavBarFlags = view.getSystemUiVisibility(); if (ColorUtilKt.isBrightColor(color)) { view.setSystemUiVisibility(mOriginalNavBarFlags | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); } else { view.setSystemUiVisibility(mOriginalNavBarFlags & ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); } } @SuppressWarnings("deprecation") private void clearNavigationBarColor() { final SettingsValues settingsValues = mSettings.getCurrent(); if (!settingsValues.mCustomNavBarColor) return; final Window window = getWindow().getWindow(); if (window == null) { return; } window.setNavigationBarColor(mOriginalNavBarColor); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return; final View view = window.getDecorView(); view.setSystemUiVisibility(mOriginalNavBarFlags); } // On HUAWEI devices with Android 12: a white bar may appear in landscape mode (issue #231) // We therefore need to make the color of the status bar transparent private void workaroundForHuaweiStatusBarIssue() { final Window window = getWindow().getWindow(); if (window == null) { return; } if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S && Build.MANUFACTURER.equals("HUAWEI")) { window.setStatusBarColor(Color.TRANSPARENT); } } @SuppressLint("SwitchIntDef") @Override public void onTrimMemory(int level) { super.onTrimMemory(level); switch (level) { case TRIM_MEMORY_RUNNING_LOW, TRIM_MEMORY_RUNNING_CRITICAL, TRIM_MEMORY_COMPLETE -> KeyboardLayoutSet.onSystemLocaleChanged(); // clears caches, nothing else // deallocateMemory always called on hiding, and should not be called when showing } } }