Android Intercept Soft Keystrokes From My Own Application - Backspace Issue

1.2k views Asked by At

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;
}
1

There are 1 answers

2
Eliezer On BEST ANSWER

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;
    }
}