content: /*
   * Copyright (C) 2008 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.amnesica.kryptey.inputmethod.latin;
  
  import android.app.AlertDialog;
  import android.content.BroadcastReceiver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.content.SharedPreferences;
  import android.content.res.Configuration;
  import android.content.res.Resources;
  import android.inputmethodservice.InputMethodService;
  import android.media.AudioManager;
  import android.os.Build;
  import android.os.Debug;
  import android.os.IBinder;
  import android.os.Message;
  import android.text.InputType;
  import android.text.TextUtils;
  import android.util.Log;
  import android.util.PrintWriterPrinter;
  import android.util.Printer;
  import android.view.Gravity;
  import android.view.KeyEvent;
  import android.view.View;
  import android.view.ViewGroup.LayoutParams;
  import android.view.Window;
  import android.view.inputmethod.EditorInfo;
  
  import com.amnesica.kryptey.inputmethod.R;
  import com.amnesica.kryptey.inputmethod.compat.EditorInfoCompatUtils;
  import com.amnesica.kryptey.inputmethod.compat.PreferenceManagerCompat;
  import com.amnesica.kryptey.inputmethod.compat.ViewOutlineProviderCompatUtils;
  import com.amnesica.kryptey.inputmethod.compat.ViewOutlineProviderCompatUtils.InsetsUpdater;
  import com.amnesica.kryptey.inputmethod.event.Event;
  import com.amnesica.kryptey.inputmethod.event.InputTransaction;
  import com.amnesica.kryptey.inputmethod.keyboard.Keyboard;
  import com.amnesica.kryptey.inputmethod.keyboard.KeyboardActionListener;
  import com.amnesica.kryptey.inputmethod.keyboard.KeyboardId;
  import com.amnesica.kryptey.inputmethod.keyboard.KeyboardSwitcher;
  import com.amnesica.kryptey.inputmethod.keyboard.MainKeyboardView;
  import com.amnesica.kryptey.inputmethod.latin.common.Constants;
  import com.amnesica.kryptey.inputmethod.latin.define.DebugFlags;
  import com.amnesica.kryptey.inputmethod.latin.e2ee.E2EEStripView;
  import com.amnesica.kryptey.inputmethod.latin.inputlogic.InputLogic;
  import com.amnesica.kryptey.inputmethod.latin.settings.Settings;
  import com.amnesica.kryptey.inputmethod.latin.settings.SettingsActivity;
  import com.amnesica.kryptey.inputmethod.latin.settings.SettingsValues;
  import com.amnesica.kryptey.inputmethod.latin.utils.ApplicationUtils;
  import com.amnesica.kryptey.inputmethod.latin.utils.LeakGuardHandlerWrapper;
  import com.amnesica.kryptey.inputmethod.latin.utils.ResourceUtils;
  import com.amnesica.kryptey.inputmethod.latin.utils.ViewLayoutUtils;
  import com.amnesica.kryptey.inputmethod.signalprotocol.SignalProtocolMain;
  
  import java.io.FileDescriptor;
  import java.io.PrintWriter;
  import java.util.Locale;
  import java.util.concurrent.TimeUnit;
  
  /**
   * Input method implementation for Qwerty'ish keyboard.
   */
  public class LatinIME extends InputMethodService implements KeyboardActionListener, E2EEStripView.Listener,
      RichInputMethodManager.SubtypeChangedListener {
    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_DEALLOCATE_MEMORY_MILLIS = TimeUnit.SECONDS.toMillis(10);
  
    final Settings mSettings;
    private Locale mLocale;
    private int mOriginalNavBarColor = 0;
    private int mOriginalNavBarFlags = 0;
    final InputLogic mInputLogic = new InputLogic(this /* LatinIME */);
  
    // TODO: Move these {@link View}s to {@link KeyboardSwitcher}.
    private View mInputView;
    private InsetsUpdater mInsetsUpdater;
  
    private RichInputMethodManager mRichImm;
    final KeyboardSwitcher mKeyboardSwitcher;
  
    private AlertDialog mOptionsDialog;
  
    private E2EEStripView mE2EEStripView;
    private RichInputConnection mRichInputConnection;
  
    public final UIHandler mHandler = new UIHandler(this);
  
    public static final class UIHandler extends LeakGuardHandlerWrapper<LatinIME> {
      private static final int MSG_UPDATE_SHIFT_STATE = 0;
      private static final int MSG_PENDING_IMS_CALLBACK = 1;
      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 ARG1_TRUE = 1;
  
      private int mDelayInMillisecondsToUpdateShiftState;
  
      public UIHandler(final LatinIME ownerInstance) {
        super(ownerInstance);
      }
  
      public void onCreate() {
        final LatinIME latinIme = getOwnerInstance();
        if (latinIme == null) {
          return;
        }
        final Resources res = latinIme.getResources();
        mDelayInMillisecondsToUpdateShiftState = res.getInteger(
            R.integer.config_delay_in_milliseconds_to_update_shift_state);
      }
  
      @Override
      public void handleMessage(final Message msg) {
        final LatinIME latinIme = getOwnerInstance();
        if (latinIme == null) {
          return;
        }
        final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher;
        switch (msg.what) {
          case MSG_UPDATE_SHIFT_STATE:
            switcher.requestUpdatingShiftState(latinIme.getCurrentAutoCapsState(),
                latinIme.getCurrentRecapitalizeState());
            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;
        }
      }
  
      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 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);
      }
  
      // Working variables for the following methods.
      private boolean mIsOrientationChanging;
      private boolean mPendingSuccessiveImsCallback;
      private boolean mHasPendingStartInput;
      private boolean mHasPendingFinishInputView;
      private boolean mHasPendingFinishInput;
      private EditorInfo mAppliedEditorInfo;
  
      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();
          }
        }
      }
    }
  
    public LatinIME() {
      super();
      mSettings = Settings.getInstance();
      mKeyboardSwitcher = KeyboardSwitcher.getInstance();
    }
  
    @Override
    public void onCreate() {
      Settings.init(this);
      DebugFlags.init(PreferenceManagerCompat.getDeviceSharedPreferences(this));
      RichInputMethodManager.init(this);
      mRichImm = RichInputMethodManager.getInstance();
      mRichImm.setSubtypeChangeHandler(this);
      KeyboardSwitcher.init(this);
      AudioAndHapticFeedbackManager.init(this);
      super.onCreate();
  
      mHandler.onCreate();
  
      // TODO: Resolve mutual dependencies of {@link #loadSettings()} and
      // {@link #resetDictionaryFacilitatorIfNecessary()}.
      loadSettings();
  
      // Register to receive ringer mode change.
      final IntentFilter filter = new IntentFilter();
      filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
      registerReceiver(mRingerModeChangeReceiver, filter);
    }
  
    private boolean isFirstRunAfterInstall() {
      final SharedPreferences prefs = PreferenceManagerCompat.getDeviceSharedPreferences(this);
      Log.i(TAG, "Checking boolean firstrun: " + prefs.getBoolean("firstrun", true));
      return prefs.getBoolean("firstrun", true);
    }
  
    private void setBooleanFirstRunAfterInstall() {
      final SharedPreferences prefs = PreferenceManagerCompat.getDeviceSharedPreferences(this);
      Log.i(TAG, "Setting boolean firstrun: " + false);
      prefs.edit().putBoolean("firstrun", false).apply();
    }
  
    private void loadSettings() {
      mLocale = mRichImm.getCurrentSubtype().getLocaleObject();
      final EditorInfo editorInfo = getCurrentInputEditorInfo();
      final InputAttributes inputAttributes = new InputAttributes(editorInfo, isFullscreenMode());
      mSettings.loadSettings(inputAttributes);
      final SettingsValues currentSettingsValues = mSettings.getCurrent();
      AudioAndHapticFeedbackManager.getInstance().onSettingsChanged(currentSettingsValues);
    }
  
    @Override
    public void onDestroy() {
      mSettings.onDestroy();
      unregisterReceiver(mRingerModeChangeReceiver);
      if (mE2EEStripView != null) {
        mE2EEStripView.clear();
      }
      super.onDestroy();
    }
  
    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();
      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();
      }
  
      mKeyboardSwitcher.updateKeyboardTheme(conf.uiMode);
  
      super.onConfigurationChanged(conf);
    }
  
    @Override
    public View onCreateInputView() {
      return mKeyboardSwitcher.onCreateInputView(getResources().getConfiguration().uiMode);
    }
  
    @Override
    public void setInputView(final View view) {
      super.setInputView(view);
      mInputView = view;
      mInsetsUpdater = ViewOutlineProviderCompatUtils.setInsetsOutlineProvider(view);
      updateSoftInputWindowLayoutParameters();
  
      if (isFirstRunAfterInstall()) {
        // initialize protocol on first app run or when in debug mode
        SignalProtocolMain.initialize(this);
        setBooleanFirstRunAfterInstall();
      } else {
        // load account from shared preferences
        SignalProtocolMain.reloadAccount(this);
      }
  
      mE2EEStripView = view.findViewById(R.id.e2ee_strip_view);
      if (mE2EEStripView != null) {
        mE2EEStripView.setListener(this, view);
        mE2EEStripView.setRichInputConnection(mRichInputConnection);
      }
    }
  
    @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);
    }
  
    @Override
    public void onFinishInputView(final boolean finishingInput) {
      mRichImm.resetSubtypeCycleOrder();
      mHandler.onFinishInputView(finishingInput);
    }
  
    @Override
    public void onFinishInput() {
      mHandler.onFinishInput();
    }
  
    @Override
    public void onCurrentSubtypeChanged() {
      mInputLogic.onSubtypeChanged();
      loadKeyboard();
    }
  
    void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) {
      super.onStartInput(editorInfo, restarting);
  
      // If the primary hint language does not match the current subtype language, then try
      // to switch to the primary hint language.
      // TODO: Support all the locales in EditorInfo#hintLocales.
      final Locale primaryHintLocale = EditorInfoCompatUtils.getPrimaryHintLocale(editorInfo);
      if (primaryHintLocale == null) {
        return;
      }
      mRichImm.setCurrentSubtype(primaryHintLocale);
    }
  
    void onStartInputViewInternal(final EditorInfo editorInfo, final boolean restarting) {
      super.onStartInputView(editorInfo, restarting);
  
      // Switch to the null consumer to handle cases leading to early exit below, for which we
      // also wouldn't be consuming gesture data.
      final KeyboardSwitcher switcher = mKeyboardSwitcher;
      switcher.updateKeyboardTheme(getResources().getConfiguration().uiMode);
      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;
      }
  
      final boolean inputTypeChanged = !currentSettingsValues.isSameInputType(editorInfo);
      final boolean isDifferentTextField = !restarting || inputTypeChanged;
  
      // 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;
      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();
  
        // TODO[IL]: Can the following be moved to InputLogic#startInput?
        if (!mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess(
            editorInfo.initialSelStart, editorInfo.initialSelEnd)) {
          // 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 {
          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();
  
        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());
      }
  
      if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
    }
  
    @Override
    public void onWindowShown() {
      super.onWindowShown();
      if (isInputViewShown())
        setNavigationBarColor();
    }
  
    @Override
    public void onWindowHidden() {
      super.onWindowHidden();
      final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
      if (mainKeyboardView != null) {
        mainKeyboardView.closing();
      }
      clearNavigationBarColor();
    }
  
    void onFinishInputInternal() {
      super.onFinishInput();
  
      final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
      if (mainKeyboardView != null) {
        mainKeyboardView.closing();
      }
    }
  
    void onFinishInputViewInternal(final boolean finishingInput) {
      super.onFinishInputView(finishingInput);
    }
  
    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.
      if (isInputViewShown()
          && mInputLogic.onUpdateSelection(newSelStart, newSelEnd)) {
        mKeyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(),
            getCurrentRecapitalizeState());
      }
    }
  
    @Override
    public void hideWindow() {
      mKeyboardSwitcher.onHideWindow();
  
      if (TRACE) Debug.stopMethodTracing();
      if (isShowingOptionDialog()) {
        mOptionsDialog.dismiss();
        mOptionsDialog = null;
      }
      super.hideWindow();
    }
  
    @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 SettingsValues settingsValues = mSettings.getCurrent();
      final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView();
      if (visibleKeyboardView == null || mE2EEStripView == null) {
        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;
      }
  
      // Make e2ee strip view visible (no hide required as it will always be active)
      mE2EEStripView.setVisibility(View.VISIBLE);
  
      final int e2eeOptionsHeight = (mE2EEStripView.getVisibility() == View.VISIBLE)
          ? mE2EEStripView.getHeight() : 0;
      final int visibleTopY = inputHeight - visibleKeyboardView.getHeight() - e2eeOptionsHeight;
      // 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.isShowingMoreKeysPanel() ? 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);
    }
  
    @Override
    public boolean onShowInputRequested(final int flags, final boolean configChange) {
      // TODO set editText in E2EE here to unfocused
      if (mE2EEStripView != null) mE2EEStripView.clearFocusEditTextView();
  
      if (isImeSuppressedByHardwareKeyboard()) {
        return true;
      }
      return super.onShowInputRequested(flags, configChange);
    }
  
    @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.readUseFullscreenMode(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();
        return !(ei != null && ((ei.imeOptions & EditorInfo.IME_FLAG_NO_EXTRACT_UI) != 0));
      }
      return false;
    }
  
    @Override
    public void updateFullscreenMode() {
      super.updateFullscreenMode();
      updateSoftInputWindowLayoutParameters();
    }
  
    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();
      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);
      }
    }
  
    int getCurrentAutoCapsState() {
      return mInputLogic.getCurrentAutoCapsState(mSettings.getCurrent(),
          mRichImm.getCurrentSubtype().getKeyboardLayoutSet());
    }
  
    int getCurrentRecapitalizeState() {
      return mInputLogic.getCurrentRecapitalizeState();
    }
  
    @Override
    public boolean onCustomRequest(final int requestCode) {
      switch (requestCode) {
        case Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER:
          return showInputMethodPicker();
      }
      return false;
    }
  
    private boolean showInputMethodPicker() {
      if (isShowingOptionDialog()) {
        return false;
      }
      mOptionsDialog = mRichImm.showSubtypePicker(this,
          mKeyboardSwitcher.getMainKeyboardView().getWindowToken(), this);
      return mOptionsDialog != null;
    }
  
    public Locale getCurrentLayoutLocale() {
      return mLocale;
    }
  
    @Override
    public void onMovePointer(int steps) {
      if (mInputLogic.mConnection.hasCursorPosition()) {
        if (TextUtils.getLayoutDirectionFromLocale(getCurrentLayoutLocale()) == View.LAYOUT_DIRECTION_RTL)
          steps = -steps;
  
        steps = mInputLogic.mConnection.getUnicodeSteps(steps, true);
        final int end = mInputLogic.mConnection.getExpectedSelectionEnd() + steps;
        final int start = mInputLogic.mConnection.hasSelection() ? mInputLogic.mConnection.getExpectedSelectionStart() : end;
        mInputLogic.mConnection.setSelection(start, end);
      } else {
        for (; steps < 0; steps++)
          mInputLogic.sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT);
        for (; steps > 0; steps--)
          mInputLogic.sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT);
      }
    }
  
    @Override
    public void onMoveDeletePointer(int steps) {
      if (mInputLogic.mConnection.hasCursorPosition()) {
        steps = mInputLogic.mConnection.getUnicodeSteps(steps, false);
        final int end = mInputLogic.mConnection.getExpectedSelectionEnd();
        final int start = mInputLogic.mConnection.getExpectedSelectionStart() + steps;
        if (start > end)
          return;
        mInputLogic.mConnection.setSelection(start, end);
      } else {
        for (; steps < 0; steps++)
          mInputLogic.sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
      }
    }
  
    @Override
    public void onUpWithDeletePointerActive() {
      if (mInputLogic.mConnection.hasSelection())
        mInputLogic.sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
    }
  
    private boolean isShowingOptionDialog() {
      return mOptionsDialog != null && mOptionsDialog.isShowing();
    }
  
    public void switchToNextSubtype() {
      final IBinder token = getWindow().getWindow().getAttributes().token;
      mRichImm.switchToNextInputMethod(token, !shouldSwitchToOtherInputMethods(token));
    }
  
    // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for
    // alphabetic shift and shift while in symbol layout and get rid of this method.
    private int getCodePointForKeyboard(final int codePoint) {
      if (Constants.CODE_SHIFT == codePoint) {
        final Keyboard currentKeyboard = mKeyboardSwitcher.getKeyboard();
        if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) {
          return codePoint;
        }
        return Constants.CODE_SYMBOL_SHIFT;
      }
      return codePoint;
    }
  
    // Implementation of {@link KeyboardActionListener}.
    public void onCodeInput(final int codePoint, final int x, final int y,
                            final boolean isKeyRepeat) {
      // 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(getCodePointForKeyboard(codePoint),
          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(final Event event) {
      final InputTransaction completeInputTransaction =
          mInputLogic.onCodeInput(mSettings.getCurrent(), event);
      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
    public static Event createSoftwareKeypressEvent(final int keyCodeOrCodePoint, 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, keyX, keyY, isKeyRepeat);
    }
  
    // Called from PointerTracker through the KeyboardActionListener interface
    @Override
    public void onTextInput(String rawText) {
      // TODO: have the keyboard pass the correct key code when we need it.
      final Event event = Event.createSoftwareTextEvent(rawText, Constants.CODE_OUTPUT_TEXT);
      final InputTransaction completeInputTransaction =
          mInputLogic.onTextInput(mSettings.getCurrent(), event);
      updateStateAfterInputTransaction(completeInputTransaction);
      mKeyboardSwitcher.onEvent(event, getCurrentAutoCapsState(), getCurrentRecapitalizeState());
    }
  
    // Called from PointerTracker through the KeyboardActionListener interface
    @Override
    public void onFinishSlidingInput() {
      // User finished sliding input.
      mKeyboardSwitcher.onFinishSlidingInput(getCurrentAutoCapsState(),
          getCurrentRecapitalizeState());
    }
  
    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.
      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();
          break;
        case InputTransaction.SHIFT_UPDATE_NOW:
          mKeyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(),
              getCurrentRecapitalizeState());
          break;
        default: // SHIFT_NO_UPDATE
      }
    }
  
    private 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 == Constants.CODE_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);
    }
  
    // Callback of the {@link KeyboardActionListener}. This is called when a key is depressed;
    // release matching call is {@link #onReleaseKey(int,boolean)} below.
    @Override
    public void onPressKey(final int primaryCode, final int repeatCount,
                           final boolean isSinglePointer) {
      mKeyboardSwitcher.onPressKey(primaryCode, isSinglePointer, getCurrentAutoCapsState(),
          getCurrentRecapitalizeState());
      hapticAndAudioFeedback(primaryCode, repeatCount);
    }
  
    // Callback of the {@link KeyboardActionListener}. This is called when a key is released;
    // press matching call is {@link #onPressKey(int,int,boolean)} above.
    @Override
    public void onReleaseKey(final int primaryCode, final boolean withSliding) {
      mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding, getCurrentAutoCapsState(),
          getCurrentRecapitalizeState());
    }
  
    // 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 (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
          AudioAndHapticFeedbackManager.getInstance().onRingerModeChanged();
        }
      }
    };
  
    public void launchSettings() {
      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 debugDumpStateAndCrashWithException(final String context) {
      final SettingsValues settingsValues = mSettings.getCurrent();
      final StringBuilder s = new StringBuilder(settingsValues.toString());
      s.append("\nAttributes : ").append(settingsValues.mInputAttributes)
          .append("\nContext : ").append(context);
      throw new RuntimeException(s.toString());
    }
  
    @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 = " + ApplicationUtils.getVersionCode(this));
      p.println("  VersionName = " + ApplicationUtils.getVersionName(this));
      final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
      final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1;
      p.println("  Keyboard mode = " + keyboardMode);
    }
  
    public boolean shouldSwitchToOtherInputMethods(final IBinder token) {
      // TODO: Revisit here to reorganize the settings. Probably we can/should use different
      // strategy once the implementation of
      // {@link InputMethodManager#shouldOfferSwitchingToNextInputMethod} is defined well.
      if (!mSettings.getCurrent().mImeSwitchEnabled) {
        return false;
      }
      return mRichImm.shouldOfferSwitchingToOtherInputMethods(token);
    }
  
    public boolean shouldShowLanguageSwitchKey() {
      if (mSettings.getCurrent().isLanguageSwitchKeyDisabled()) {
        return false;
      }
      if (mRichImm.hasMultipleEnabledSubtypes()) {
        return true;
      }
  
      final IBinder token = getWindow().getWindow().getAttributes().token;
      if (token == null) {
        return false;
      }
      return shouldSwitchToOtherInputMethods(token);
    }
  
    private void setNavigationBarColor() {
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && mSettings.getCurrent().mUseMatchingNavbarColor) {
        final SharedPreferences prefs = PreferenceManagerCompat.getDeviceSharedPreferences(this);
        final int keyboardColor = Settings.readKeyboardColor(prefs, this);
        final Window window = getWindow().getWindow();
        if (window == null) {
          return;
        }
        mOriginalNavBarColor = window.getNavigationBarColor();
        window.setNavigationBarColor(keyboardColor);
  
        final View view = window.getDecorView();
        mOriginalNavBarFlags = view.getSystemUiVisibility();
        if (ResourceUtils.isBrightColor(keyboardColor)) {
          view.setSystemUiVisibility(mOriginalNavBarFlags | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
        } else {
          view.setSystemUiVisibility(mOriginalNavBarFlags & ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
        }
      }
    }
  
    private void clearNavigationBarColor() {
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && mSettings.getCurrent().mUseMatchingNavbarColor) {
        final Window window = getWindow().getWindow();
        if (window == null) {
          return;
        }
        window.setNavigationBarColor(mOriginalNavBarColor);
        final View view = window.getDecorView();
        view.setSystemUiVisibility(mOriginalNavBarFlags);
      }
    }
  
    public void setRichInputConnection(RichInputConnection richInputConnection) {
      mRichInputConnection = richInputConnection;
    }
  }
  