content: /*
   * Copyright (C) 2013 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.inputlogic;
  
  import android.os.SystemClock;
  import android.text.TextUtils;
  import android.view.KeyCharacterMap;
  import android.view.KeyEvent;
  import android.view.inputmethod.EditorInfo;
  
  import com.amnesica.kryptey.inputmethod.event.Event;
  import com.amnesica.kryptey.inputmethod.event.InputTransaction;
  import com.amnesica.kryptey.inputmethod.latin.LatinIME;
  import com.amnesica.kryptey.inputmethod.latin.RichInputConnection;
  import com.amnesica.kryptey.inputmethod.latin.common.Constants;
  import com.amnesica.kryptey.inputmethod.latin.common.StringUtils;
  import com.amnesica.kryptey.inputmethod.latin.settings.SettingsValues;
  import com.amnesica.kryptey.inputmethod.latin.utils.InputTypeUtils;
  import com.amnesica.kryptey.inputmethod.latin.utils.RecapitalizeStatus;
  import com.amnesica.kryptey.inputmethod.latin.utils.SubtypeLocaleUtils;
  
  import java.util.TreeSet;
  
  /**
   * This class manages the input logic.
   */
  public final class InputLogic {
    // TODO : Remove this member when we can.
    final LatinIME mLatinIME;
  
    // This has package visibility so it can be accessed from InputLogicHandler.
    public final RichInputConnection mConnection;
    private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus();
  
    public final TreeSet<Long> mCurrentlyPressedHardwareKeys = new TreeSet<>();
  
    /**
     * Create a new instance of the input logic.
     *
     * @param latinIME the instance of the parent LatinIME. We should remove this when we can.
     *                 dictionary.
     */
    public InputLogic(final LatinIME latinIME) {
      mLatinIME = latinIME;
      mConnection = new RichInputConnection(latinIME);
      // TODO handle better
      mLatinIME.setRichInputConnection(mConnection);
    }
  
    /**
     * Initializes the input logic for input in an editor.
     * <p>
     * Call this when input starts or restarts in some editor (typically, in onStartInputView).
     */
    public void startInput() {
      mRecapitalizeStatus.disable(); // Do not perform recapitalize until the cursor is moved once
      mCurrentlyPressedHardwareKeys.clear();
    }
  
    /**
     * Call this when the subtype changes.
     */
    public void onSubtypeChanged() {
      startInput();
    }
  
    /**
     * React to a string input.
     * <p>
     * This is triggered by keys that input many characters at once, like the ".com" key or
     * some additional keys for example.
     *
     * @param settingsValues the current values of the settings.
     * @param event          the input event containing the data.
     * @return the complete transaction object
     */
    public InputTransaction onTextInput(final SettingsValues settingsValues, final Event event) {
      final String rawText = event.getTextToCommit().toString();
      final InputTransaction inputTransaction = new InputTransaction(settingsValues);
      mConnection.beginBatchEdit();
      final String text = performSpecificTldProcessingOnTextInput(rawText);
      mConnection.commitText(text, 1);
      mConnection.endBatchEdit();
      // Space state must be updated before calling updateShiftState
      inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
      return inputTransaction;
    }
  
    /**
     * Consider an update to the cursor position. Evaluate whether this update has happened as
     * part of normal typing or whether it was an explicit cursor move by the user. In any case,
     * do the necessary adjustments.
     *
     * @param newSelStart new selection start
     * @param newSelEnd   new selection end
     * @return whether the cursor has moved as a result of user interaction.
     */
    public boolean onUpdateSelection(final int newSelStart, final int newSelEnd) {
      resetEntireInputState(newSelStart, newSelEnd);
  
      // The cursor has been moved : we now accept to perform recapitalization
      mRecapitalizeStatus.enable();
      // Stop the last recapitalization, if started.
      mRecapitalizeStatus.stop();
      return true;
    }
  
    /**
     * React to a code input. It may be a code point to insert, or a symbolic value that influences
     * the keyboard behavior.
     * <p>
     * Typically, this is called whenever a key is pressed on the software keyboard. This is not
     * the entry point for gesture input; see the onBatchInput* family of functions for this.
     *
     * @param settingsValues the current settings values.
     * @param event          the event to handle.
     * @return the complete transaction object
     */
    public InputTransaction onCodeInput(final SettingsValues settingsValues, final Event event) {
      final InputTransaction inputTransaction = new InputTransaction(settingsValues);
      mConnection.beginBatchEdit();
  
      Event currentEvent = event;
      while (null != currentEvent) {
        if (currentEvent.isConsumed()) {
          handleConsumedEvent(currentEvent);
        } else if (currentEvent.isFunctionalKeyEvent()) {
          handleFunctionalEvent(currentEvent, inputTransaction);
        } else {
          handleNonFunctionalEvent(currentEvent, inputTransaction);
        }
        currentEvent = currentEvent.mNextEvent;
      }
      mConnection.endBatchEdit();
      return inputTransaction;
    }
  
    /**
     * Handle a consumed event.
     * <p>
     * Consumed events represent events that have already been consumed, typically by the
     * combining chain.
     *
     * @param event The event to handle.
     */
    private void handleConsumedEvent(final Event event) {
      // A consumed event may have text to commit and an update to the composing state, so
      // we evaluate both. With some combiners, it's possible than an event contains both
      // and we enter both of the following if clauses.
      final CharSequence textToCommit = event.getTextToCommit();
      if (!TextUtils.isEmpty(textToCommit)) {
        mConnection.commitText(textToCommit, 1);
      }
    }
  
    /**
     * Handle a functional key event.
     * <p>
     * A functional event is a special key, like delete, shift, emoji, or the settings key.
     * Non-special keys are those that generate a single code point.
     * This includes all letters, digits, punctuation, separators, emoji. It excludes keys that
     * manage keyboard-related stuff like shift, language switch, settings, layout switch, or
     * any key that results in multiple code points like the ".com" key.
     *
     * @param event            The event to handle.
     * @param inputTransaction The transaction in progress.
     */
    private void handleFunctionalEvent(final Event event, final InputTransaction inputTransaction) {
      switch (event.mKeyCode) {
        case Constants.CODE_DELETE:
          handleBackspaceEvent(event, inputTransaction);
          // Backspace is a functional key, but it affects the contents of the editor.
          break;
        case Constants.CODE_SHIFT:
          performRecapitalization(inputTransaction.mSettingsValues);
          inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
          break;
        case Constants.CODE_CAPSLOCK:
          // Note: Changing keyboard to shift lock state is handled in
          // {@link KeyboardSwitcher#onEvent(Event)}.
          break;
        case Constants.CODE_SYMBOL_SHIFT:
          // Note: Calling back to the keyboard on the symbol Shift key is handled in
          // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
          break;
        case Constants.CODE_SWITCH_ALPHA_SYMBOL:
          // Note: Calling back to the keyboard on symbol key is handled in
          // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
          break;
        case Constants.CODE_SETTINGS:
          onSettingsKeyPressed();
          break;
        case Constants.CODE_ACTION_NEXT:
          performEditorAction(EditorInfo.IME_ACTION_NEXT);
          break;
        case Constants.CODE_ACTION_PREVIOUS:
          performEditorAction(EditorInfo.IME_ACTION_PREVIOUS);
          break;
        case Constants.CODE_LANGUAGE_SWITCH:
          handleLanguageSwitchKey();
          break;
        case Constants.CODE_SHIFT_ENTER:
          final Event tmpEvent = Event.createSoftwareKeypressEvent(Constants.CODE_ENTER,
              event.mKeyCode, event.mX, event.mY, event.isKeyRepeat());
          handleNonSpecialCharacterEvent(tmpEvent, inputTransaction);
          // Shift + Enter is treated as a functional key but it results in adding a new
          // line, so that does affect the contents of the editor.
          break;
        default:
          throw new RuntimeException("Unknown key code : " + event.mKeyCode);
      }
    }
  
    /**
     * Handle an event that is not a functional event.
     * <p>
     * These events are generally events that cause input, but in some cases they may do other
     * things like trigger an editor action.
     *
     * @param event            The event to handle.
     * @param inputTransaction The transaction in progress.
     */
    private void handleNonFunctionalEvent(final Event event,
                                          final InputTransaction inputTransaction) {
      switch (event.mCodePoint) {
        case Constants.CODE_ENTER:
          final EditorInfo editorInfo = getCurrentInputEditorInfo();
          final int imeOptionsActionId =
              InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo);
          if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) {
            // Either we have an actionLabel and we should performEditorAction with
            // actionId regardless of its value.
            performEditorAction(editorInfo.actionId);
          } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) {
            // We didn't have an actionLabel, but we had another action to execute.
            // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast,
            // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it
            // means there should be an action and the app didn't bother to set a specific
            // code for it - presumably it only handles one. It does not have to be treated
            // in any specific way: anything that is not IME_ACTION_NONE should be sent to
            // performEditorAction.
            performEditorAction(imeOptionsActionId);
          } else {
            // No action label, and the action from imeOptions is NONE: this is a regular
            // enter key that should input a carriage return.
            handleNonSpecialCharacterEvent(event, inputTransaction);
          }
          break;
        default:
          handleNonSpecialCharacterEvent(event, inputTransaction);
          break;
      }
    }
  
    /**
     * Handle inputting a code point to the editor.
     * <p>
     * Non-special keys are those that generate a single code point.
     * This includes all letters, digits, punctuation, separators, emoji. It excludes keys that
     * manage keyboard-related stuff like shift, language switch, settings, layout switch, or
     * any key that results in multiple code points like the ".com" key.
     *
     * @param event            The event to handle.
     * @param inputTransaction The transaction in progress.
     */
    private void handleNonSpecialCharacterEvent(final Event event,
                                                final InputTransaction inputTransaction) {
      final int codePoint = event.mCodePoint;
      if (inputTransaction.mSettingsValues.isWordSeparator(codePoint)
          || Character.getType(codePoint) == Character.OTHER_SYMBOL) {
        handleSeparatorEvent(event, inputTransaction);
      } else {
        handleNonSeparatorEvent(event);
      }
    }
  
    /**
     * Handle a non-separator.
     *
     * @param event The event to handle.
     */
    private void handleNonSeparatorEvent(final Event event) {
      mConnection.commitText(StringUtils.newSingleCodePointString(event.mCodePoint), 1);
    }
  
    /**
     * Handle input of a separator code point.
     *
     * @param event            The event to handle.
     * @param inputTransaction The transaction in progress.
     */
    private void handleSeparatorEvent(final Event event, final InputTransaction inputTransaction) {
      mConnection.commitText(StringUtils.newSingleCodePointString(event.mCodePoint), 1);
  
      inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
    }
  
    /**
     * Handle a press on the backspace key.
     *
     * @param event            The event to handle.
     * @param inputTransaction The transaction in progress.
     */
    private void handleBackspaceEvent(final Event event, final InputTransaction inputTransaction) {
      // In many cases after backspace, we need to update the shift state. Normally we need
      // to do this right away to avoid the shift state being out of date in case the user types
      // backspace then some other character very fast. However, in the case of backspace key
      // repeat, this can lead to flashiness when the cursor flies over positions where the
      // shift state should be updated, so if this is a key repeat, we update after a small delay.
      // Then again, even in the case of a key repeat, if the cursor is at start of text, it
      // can't go any further back, so we can update right away even if it's a key repeat.
      final int shiftUpdateKind =
          event.isKeyRepeat() && mConnection.getExpectedSelectionStart() > 0
              ? InputTransaction.SHIFT_UPDATE_LATER : InputTransaction.SHIFT_UPDATE_NOW;
      inputTransaction.requireShiftUpdate(shiftUpdateKind);
  
      // No cancelling of commit/double space/swap: we have a regular backspace.
      // We should backspace one char and restart suggestion if at the end of a word.
      if (mConnection.hasSelection()) {
        // If there is a selection, remove it.
        // We also need to unlearn the selected text.
        final int numCharsDeleted = mConnection.getExpectedSelectionEnd()
            - mConnection.getExpectedSelectionStart();
        mConnection.setSelection(mConnection.getExpectedSelectionEnd(),
            mConnection.getExpectedSelectionEnd());
        mConnection.deleteTextBeforeCursor(numCharsDeleted);
      } else {
        // There is no selection, just delete one character.
        if (inputTransaction.mSettingsValues.mInputAttributes.isTypeNull()
            || Constants.NOT_A_CURSOR_POSITION
            == mConnection.getExpectedSelectionEnd()) {
          // There are three possible reasons to send a key event: either the field has
          // type TYPE_NULL, in which case the keyboard should send events, or we are
          // running in backward compatibility mode, or we don't know the cursor position.
          // Before Jelly bean, the keyboard would simulate a hardware keyboard event on
          // pressing enter or delete. This is bad for many reasons (there are race
          // conditions with commits) but some applications are relying on this behavior
          // so we continue to support it for older apps, so we retain this behavior if
          // the app has target SDK < JellyBean.
          // As for the case where we don't know the cursor position, it can happen
          // because of bugs in the framework. But the framework should know, so the next
          // best thing is to leave it to whatever it thinks is best.
          sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
        } else {
          final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
          if (codePointBeforeCursor == Constants.NOT_A_CODE) {
            // HACK for backward compatibility with broken apps that haven't realized
            // yet that hardware keyboards are not the only way of inputting text.
            // Nothing to delete before the cursor. We should not do anything, but many
            // broken apps expect something to happen in this case so that they can
            // catch it and have their broken interface react. If you need the keyboard
            // to do this, you're doing it wrong -- please fix your app.
            mConnection.deleteTextBeforeCursor(1);
            // TODO: Add a new StatsUtils method onBackspaceWhenNoText()
            return;
          }
          final int lengthToDelete =
              Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1;
          mConnection.deleteTextBeforeCursor(lengthToDelete);
        }
      }
    }
  
    /**
     * Handle a press on the language switch key (the "globe key")
     */
    private void handleLanguageSwitchKey() {
      mLatinIME.switchToNextSubtype();
    }
  
    /**
     * Performs a recapitalization event.
     *
     * @param settingsValues The current settings values.
     */
    private void performRecapitalization(final SettingsValues settingsValues) {
      if (!mConnection.hasSelection() || !mRecapitalizeStatus.mIsEnabled()) {
        return; // No selection or recapitalize is disabled for now
      }
      final int selectionStart = mConnection.getExpectedSelectionStart();
      final int selectionEnd = mConnection.getExpectedSelectionEnd();
      final int numCharsSelected = selectionEnd - selectionStart;
      if (numCharsSelected > Constants.MAX_CHARACTERS_FOR_RECAPITALIZATION) {
        // We bail out if we have too many characters for performance reasons. We don't want
        // to suck possibly multiple-megabyte data.
        return;
      }
      // If we have a recapitalize in progress, use it; otherwise, start a new one.
      if (!mRecapitalizeStatus.isStarted()
          || !mRecapitalizeStatus.isSetAt(selectionStart, selectionEnd)) {
        final CharSequence selectedText =
            mConnection.getSelectedText(0 /* flags, 0 for no styles */);
        if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection
        mRecapitalizeStatus.start(selectionStart, selectionEnd, selectedText.toString(), mLatinIME.getCurrentLayoutLocale());
        // We trim leading and trailing whitespace.
        mRecapitalizeStatus.trim();
      }
      mConnection.finishComposingText();
      mRecapitalizeStatus.rotate();
      mConnection.setSelection(selectionEnd, selectionEnd);
      mConnection.deleteTextBeforeCursor(numCharsSelected);
      mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0);
      mConnection.setSelection(mRecapitalizeStatus.getNewCursorStart(),
          mRecapitalizeStatus.getNewCursorEnd());
    }
  
    /**
     * Gets the current auto-caps state, factoring in the space state.
     * <p>
     * This method tries its best to do this in the most efficient possible manner. It avoids
     * getting text from the editor if possible at all.
     * This is called from the KeyboardSwitcher (through a trampoline in LatinIME) because it
     * needs to know auto caps state to display the right layout.
     *
     * @param settingsValues the relevant settings values
     * @param layoutSetName  the name of the current keyboard layout set
     * @return a caps mode from TextUtils.CAP_MODE_* or Constants.TextUtils.CAP_MODE_OFF.
     */
    public int getCurrentAutoCapsState(final SettingsValues settingsValues,
                                       final String layoutSetName) {
      if (!settingsValues.mAutoCap || !layoutUsesAutoCaps(layoutSetName)) {
        return Constants.TextUtils.CAP_MODE_OFF;
      }
  
      final EditorInfo ei = getCurrentInputEditorInfo();
      if (ei == null) return Constants.TextUtils.CAP_MODE_OFF;
      final int inputType = ei.inputType;
      // Warning: this depends on mSpaceState, which may not be the most current value. If
      // mSpaceState gets updated later, whoever called this may need to be told about it.
      return mConnection.getCursorCapsMode(inputType, settingsValues.mSpacingAndPunctuations);
    }
  
    private boolean layoutUsesAutoCaps(final String layoutSetName) {
      switch (layoutSetName) {
        case SubtypeLocaleUtils.LAYOUT_ARABIC:
        case SubtypeLocaleUtils.LAYOUT_BENGALI:
        case SubtypeLocaleUtils.LAYOUT_BENGALI_AKKHOR:
        case SubtypeLocaleUtils.LAYOUT_FARSI:
        case SubtypeLocaleUtils.LAYOUT_GEORGIAN:
        case SubtypeLocaleUtils.LAYOUT_HEBREW:
        case SubtypeLocaleUtils.LAYOUT_HINDI:
        case SubtypeLocaleUtils.LAYOUT_HINDI_COMPACT:
        case SubtypeLocaleUtils.LAYOUT_KANNADA:
        case SubtypeLocaleUtils.LAYOUT_KHMER:
        case SubtypeLocaleUtils.LAYOUT_LAO:
        case SubtypeLocaleUtils.LAYOUT_MALAYALAM:
        case SubtypeLocaleUtils.LAYOUT_MARATHI:
        case SubtypeLocaleUtils.LAYOUT_NEPALI_ROMANIZED:
        case SubtypeLocaleUtils.LAYOUT_NEPALI_TRADITIONAL:
        case SubtypeLocaleUtils.LAYOUT_TAMIL:
        case SubtypeLocaleUtils.LAYOUT_TELUGU:
        case SubtypeLocaleUtils.LAYOUT_THAI:
        case SubtypeLocaleUtils.LAYOUT_URDU:
          return false;
        default:
          return true;
      }
    }
  
    public int getCurrentRecapitalizeState() {
      if (!mRecapitalizeStatus.isStarted()
          || !mRecapitalizeStatus.isSetAt(mConnection.getExpectedSelectionStart(),
          mConnection.getExpectedSelectionEnd())) {
        // Not recapitalizing at the moment
        return RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
      }
      return mRecapitalizeStatus.getCurrentMode();
    }
  
    /**
     * @return the editor info for the current editor
     */
    private EditorInfo getCurrentInputEditorInfo() {
      return mLatinIME.getCurrentInputEditorInfo();
    }
  
    /**
     * @param actionId the action to perform
     */
    private void performEditorAction(final int actionId) {
      mConnection.performEditorAction(actionId);
    }
  
    /**
     * Perform the processing specific to inputting TLDs.
     * <p>
     * Some keys input a TLD (specifically, the ".com" key) and this warrants some specific
     * processing. First, if this is a TLD, we ignore PHANTOM spaces -- this is done by type
     * of character in onCodeInput, but since this gets inputted as a whole string we need to
     * do it here specifically. Then, if the last character before the cursor is a period, then
     * we cut the dot at the start of ".com". This is because humans tend to type "www.google."
     * and then press the ".com" key and instinctively don't expect to get "www.google..com".
     *
     * @param text the raw text supplied to onTextInput
     * @return the text to actually send to the editor
     */
    private String performSpecificTldProcessingOnTextInput(final String text) {
      if (text.length() <= 1 || text.charAt(0) != Constants.CODE_PERIOD
          || !Character.isLetter(text.charAt(1))) {
        // Not a tld: do nothing.
        return text;
      }
      final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
      // If no code point, #getCodePointBeforeCursor returns NOT_A_CODE_POINT.
      if (Constants.CODE_PERIOD == codePointBeforeCursor) {
        return text.substring(1);
      }
      return text;
    }
  
    /**
     * Handle a press on the settings key.
     */
    private void onSettingsKeyPressed() {
      mLatinIME.launchSettings();
    }
  
    /**
     * Resets the whole input state to the starting state.
     * <p>
     * This will clear the composing word, reset the last composed word, clear the suggestion
     * strip and tell the input connection about it so that it can refresh its caches.
     *
     * @param newSelStart the new selection start, in java characters.
     * @param newSelEnd   the new selection end, in java characters.
     */
    // TODO: how is this different from startInput ?!
    private void resetEntireInputState(final int newSelStart, final int newSelEnd) {
      mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart, newSelEnd);
    }
  
    /**
     * Sends a DOWN key event followed by an UP key event to the editor.
     * <p>
     * If possible at all, avoid using this method. It causes all sorts of race conditions with
     * the text view because it goes through a different, asynchronous binder. Also, batch edits
     * are ignored for key events. Use the normal software input methods instead.
     *
     * @param keyCode the key code to send inside the key event.
     */
    public void sendDownUpKeyEvent(final int keyCode) {
      final long eventTime = SystemClock.uptimeMillis();
      mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime,
          KeyEvent.ACTION_DOWN, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
          KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
      mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
          KeyEvent.ACTION_UP, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
          KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
    }
  
    /**
     * Sends a code point to the editor, using the most appropriate method.
     * <p>
     * Normally we send code points with commitText, but there are some cases (where backward
     * compatibility is a concern for example) where we want to use deprecated methods.
     *
     * @param codePoint the code point to send.
     */
    // TODO: replace these two parameters with an InputTransaction
    private void sendKeyCodePoint(final int codePoint) {
      // TODO: Remove this special handling of digit letters.
      // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
      if (codePoint >= '0' && codePoint <= '9') {
        sendDownUpKeyEvent(codePoint - '0' + KeyEvent.KEYCODE_0);
        return;
      }
  
      mConnection.commitText(StringUtils.newSingleCodePointString(codePoint), 1);
    }
  
    /**
     * Retry resetting caches in the rich input connection.
     * <p>
     * When the editor can't be accessed we can't reset the caches, so we schedule a retry.
     * This method handles the retry, and re-schedules a new retry if we still can't access.
     * We only retry up to 5 times before giving up.
     *
     * @param tryResumeSuggestions Whether we should resume suggestions or not.
     * @param remainingTries       How many times we may try again before giving up.
     * @return whether true if the caches were successfully reset, false otherwise.
     */
    public boolean retryResetCachesAndReturnSuccess(final boolean tryResumeSuggestions,
                                                    final int remainingTries, final LatinIME.UIHandler handler) {
      if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(
          mConnection.getExpectedSelectionStart(), mConnection.getExpectedSelectionEnd())) {
        if (0 < remainingTries) {
          handler.postResetCaches(tryResumeSuggestions, remainingTries - 1);
          return false;
        }
        // If remainingTries is 0, we should stop waiting for new tries, however we'll still
        // return true as we need to perform other tasks (for example, loading the keyboard).
      }
      return true;
    }
  }
  