Search code examples
androidandroid-edittextandroid-softkeyboardandroid-input-method

Android Intercept Soft Keystrokes From My Own Application - Backspace Issue


I have an application that needs to mirror every keystroke made in a certain Fragment that I have. What I am doing so far is attaching a TextWatcher to an EditText and overriding afterTextChanged and pulling the last character (the EditText will be hidden so there can only be 1 character entered at a time).

In order to handle backspaces, I override onKeyDown in the EditText and use a workaround involving a custom BaseInputConnection for Jellybean and above devices (delivers a generated KEYCODE_DEL to onKeyDown - found this solution somewhere on SO).

The issue I am having is that when the EditText is empty, no KEYCODE_DEL events are generated so I have no way of detecting a backspace (even if it wouldn't do anything). What I'm trying to do is add a single character to the EditText when I create it, and when I detect in afterTextChanged that the EditText is empty, so that if I hit backspace before entering another character, it will delete that filler character, and then repopulate it with another filler character.

However, the "filler characters" never get deleted. For example, I populate the EditText with an "a" when I instantiate it. If I press backspace, nothing happens. The "a" is not deleted from the EditText.

Anyone know what's going on here?

State variables

private static volatile boolean backspacePressed = false;
private static volatile boolean ignoreTextChange = false;

The TextWatcher

private TextWatcher textWatcher = new TextWatcher() {       
    @Override
    public void afterTextChanged(Editable s) {
        if(backspacePressed) {
            Logger.i("BKSPC");
            backspacePressed = false;
            if(et.length() <= 1) {
                ignoreTextChange = true;
                et.setText("b");
                et.setSelection(1);
            }
            return;
        }

        if(ignoreTextChange) {
            ignoreTextChange = false;
            return;
        }
        else {
            Logger.i("" + s.charAt(s.length() - 1));    
        }
    }

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {}

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {}
};  

The custom EditText

public class InterceptTextView extends EditText {

    public InterceptTextView(Context context) {
        super(context);
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent key) {
        if(keyCode == KeyEvent.KEYCODE_DEL && key.getAction() == KeyEvent.ACTION_DOWN) {
            Logger.i("BCKSPC");
        }
        return false;
    }

    @Override
    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
        outAttrs.actionLabel = null;
        outAttrs.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;

        BaseInputConnection connection = new BaseInputConnection(this, false) {         

            @Override
            public boolean deleteSurroundingText (int beforeLength, int afterLength) {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                            /* In Jelly Bean, they don't send key events for delete.
                             *  Instead, they send beforeLength = 1, afterLength = 0.
                             *  So, we'll just simulate what it used to do. */
                            if (beforeLength == 0 || beforeLength == 1 && afterLength == 0) {
                                    sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_DEL);
                                    backspacePressed = true;
                                    return true;
                            }
                    }
                    return super.deleteSurroundingText(beforeLength, afterLength);
            }

            private void sendDownUpKeyEventForBackwardCompatibility (final int code) {
                    final long eventTime = SystemClock.uptimeMillis();
                    super.sendKeyEvent(new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, code, 0, 0,
                            KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
                    super.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime, KeyEvent.ACTION_UP, code, 0, 0,
                            KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
            }
    };
    return connection;
    }

}

My Fragment's onCreateView

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    et = new InterceptTextView(getActivity());
    et.addTextChangedListener(textWatcher);
    ignoreTextChange = true;
    et.setText("a");
            et.setSelection(1);
    return et;
}

Solution

  • Here's how I did it. In my InputConnection I override getTextBeforeCursor to always to return " " so that the IME always thinks that there's at least one character it can delete.

    I also set the input type of the connection to InputType.TYPE_NULL so that key events will be delivered (official workaround from Google).

    My custom EditText:

    public class InterceptTextView extends EditText {
        public interface OnBackspacePressListener {
            public void onBackspacePressed();
        }
    
        private OnBackspacePressListener backspaceListener;
    
    
        public InterceptTextView(Context context, AttributeSet attrs) {
            super(context, attrs);
            setText(" ");
        }
    
        public void setOnBackspacePressListener(OnBackspacePressListener backspaceListener) {
            this.backspaceListener = backspaceListener;
        }
    
        @Override
        public boolean onKeyDown(int keyCode, KeyEvent event) {
            if(keyCode == KeyEvent.KEYCODE_DEL) {
                if(backspaceListener != null) {
                    backspaceListener.onBackspacePressed();
                }
            }
            return super.onKeyDown(keyCode, event);
        }
    
        @Override
        public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
            outAttrs.actionLabel = null;
            outAttrs.inputType = InputType.TYPE_NULL;
    
            BaseInputConnection connection = new BaseInputConnection(this, false) {    
    
                @Override
                public String getTextBeforeCursor(int ignore, int ignore2) {
                    return " ";
                }
            };
            return connection;
        }
    }
    

    My Fragment:

    public class KeyFragment extends Fragment {
        private static volatile boolean backspacePressed = false;
    
        private InterceptTextView et;
    
        private TextWatcher textWatcher = new TextWatcher() {       
            @Override
            public void afterTextChanged(Editable s) {
                if(backspacePressed) {
                    backspacePressed = false;
                    return;
                }
                else {
                    Logger.i("" + s.charAt(s.length() - 1));    
                }
            }
    
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
    
            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {}
        };
    
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            View v = inflater.inflate(R.layout.keyboard, null);
            et = (InterceptTextView) v.findViewById(R.id.keyboard_input_sink);
            et.setOnBackspacePressListener(new OnBackspacePressListener() {
    
                @Override
                public void onBackspacePressed() {
                    backspacePressed = true;
                    Logger.i("BCKSPC");
                }
    
            });
            et.addTextChangedListener(textWatcher);
            return v;
        }
    }