content: package com.amnesica.kryptey.inputmethod.latin;
  
  import android.os.Bundle;
  import android.text.Editable;
  import android.text.Spanned;
  import android.text.method.KeyListener;
  import android.text.style.SuggestionSpan;
  import android.util.Log;
  import android.view.inputmethod.BaseInputConnection;
  import android.view.inputmethod.CompletionInfo;
  import android.view.inputmethod.CorrectionInfo;
  import android.view.inputmethod.ExtractedText;
  import android.view.inputmethod.ExtractedTextRequest;
  import android.view.inputmethod.InputConnection;
  import android.widget.TextView;
  
  /**
   * Source: https://stackoverflow.com/a/39460124
   */
  public class E2EEInputConnection extends BaseInputConnection {
    private static final boolean DEBUG = false;
    private static final String TAG = E2EEInputConnection.class.getSimpleName();
  
    private final TextView mTextView;
  
    // Keeps track of nested begin/end batch edit to ensure this connection always has a
    // balanced impact on its associated TextView.
    // A negative value means that this connection has been finished by the InputMethodManager.
    private int mBatchEditNesting;
  
    public E2EEInputConnection(TextView textview) {
      super(textview, true);
      mTextView = textview;
    }
  
    @Override
    public Editable getEditable() {
      TextView tv = mTextView;
      if (tv != null) {
        return tv.getEditableText();
      }
      return null;
    }
  
    @Override
    public boolean beginBatchEdit() {
      synchronized (this) {
        if (mBatchEditNesting >= 0) {
          mTextView.beginBatchEdit();
          mBatchEditNesting++;
          return true;
        }
      }
      return false;
    }
  
    @Override
    public boolean endBatchEdit() {
      synchronized (this) {
        if (mBatchEditNesting > 0) {
          // When the connection is reset by the InputMethodManager and reportFinish
          // is called, some endBatchEdit calls may still be asynchronously received from the
          // IME. Do not take these into account, thus ensuring that this IC's final
          // contribution to mTextView's nested batch edit count is zero.
          mTextView.endBatchEdit();
          mBatchEditNesting--;
          return true;
        }
      }
      return false;
    }
  
    @Override
    public boolean clearMetaKeyStates(int states) {
      final Editable content = getEditable();
      if (content == null) return false;
      KeyListener kl = mTextView.getKeyListener();
      if (kl != null) {
        try {
          kl.clearMetaKeyState(mTextView, content, states);
        } catch (AbstractMethodError e) {
          // This is an old listener that doesn't implement the
          // new method.
        }
      }
      return true;
    }
  
    @Override
    public boolean commitCompletion(CompletionInfo text) {
      if (DEBUG) Log.v(TAG, "commitCompletion " + text);
      mTextView.beginBatchEdit();
      mTextView.onCommitCompletion(text);
      mTextView.endBatchEdit();
      return true;
    }
  
    /**
     * Calls the {@link TextView#onCommitCorrection} method of the associated TextView.
     */
    @Override
    public boolean commitCorrection(CorrectionInfo correctionInfo) {
      if (DEBUG) Log.v(TAG, "commitCorrection" + correctionInfo);
      mTextView.beginBatchEdit();
      mTextView.onCommitCorrection(correctionInfo);
      mTextView.endBatchEdit();
      return true;
    }
  
    @Override
    public boolean performEditorAction(int actionCode) {
      if (DEBUG) Log.v(TAG, "performEditorAction " + actionCode);
      mTextView.onEditorAction(actionCode);
      return true;
    }
  
    @Override
    public boolean performContextMenuAction(int id) {
      if (DEBUG) Log.v(TAG, "performContextMenuAction " + id);
      mTextView.beginBatchEdit();
      mTextView.onTextContextMenuItem(id);
      mTextView.endBatchEdit();
      return true;
    }
  
    @Override
    public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
      if (mTextView != null) {
        ExtractedText et = new ExtractedText();
        if (mTextView.extractText(request, et)) {
          if ((flags & GET_EXTRACTED_TEXT_MONITOR) != 0) {
  //                    mTextView.setExtracting(request);
          }
          return et;
        }
      }
      return null;
    }
  
    @Override
    public boolean performPrivateCommand(String action, Bundle data) {
      mTextView.onPrivateIMECommand(action, data);
      return true;
    }
  
    @Override
    public boolean commitText(CharSequence text, int newCursorPosition) {
      if (mTextView == null) {
        return super.commitText(text, newCursorPosition);
      }
      if (text instanceof Spanned) {
        Spanned spanned = ((Spanned) text);
        SuggestionSpan[] spans = spanned.getSpans(0, text.length(), SuggestionSpan.class);
  //            mIMM.registerSuggestionSpansForNotification(spans);
      }
  
  //        mTextView.resetErrorChangedFlag();
      boolean success = super.commitText(text, newCursorPosition);
  //        mTextView.hideErrorIfUnchanged();
  
      return success;
    }
  
    @Override
    public boolean requestCursorUpdates(int cursorUpdateMode) {
      if (DEBUG) Log.v(TAG, "requestUpdateCursorAnchorInfo " + cursorUpdateMode);
  
      // It is possible that any other bit is used as a valid flag in a future release.
      // We should reject the entire request in such a case.
      final int KNOWN_FLAGS_MASK = InputConnection.CURSOR_UPDATE_IMMEDIATE | InputConnection.CURSOR_UPDATE_MONITOR;
      final int unknownFlags = cursorUpdateMode & ~KNOWN_FLAGS_MASK;
      if (unknownFlags != 0) {
        if (DEBUG) {
          Log.d(TAG, "Rejecting requestUpdateCursorAnchorInfo due to unknown flags." + " cursorUpdateMode=" + cursorUpdateMode + " unknownFlags=" + unknownFlags);
        }
        return false;
      }
  
      return false;
    }
  }
  