How do I make KeyDown and KeyUp on Android devices?
I have a question.
I’m making an app on Keystroke dynamics on Android devices.
Now I’ve created an activity with a metric string and an EditText
. I want to capture KeyDown
and KeyUp
events on a soft keyboard.
My question is, what is the best way to capture KeyUp
and KeyDown
with Java on Android? What if EditText
is a good choice? Does it have a way to capture any keystrokes?
Edit
I want to detect the key from the string above and measure the time it takes to press it (e.g. start measurement on KeyDown
and stop on KeyUp
). If possible, I want to block other keys that are not mentioned in my test string
(it's 9RJhl6aH0n
, just like on my screen).
EDIT2
What I’ve achieved so far is this, but my app crashes on default
when I code the line: measureText.setText(").
It works just fine, but it still doesn’t fire on KeyDown
(or KeyPress
). These methods run only on KeyUp
, when the user has just typed a letter. Order matters!
measureText.addTextChangedListener(new TextWatcher(){
@Override
public void afterTextChanged(Editable arg0) {
switch(measureText.getText().toString()){
case "9":
break;
case "9R":
break;
case "9RJ":
break;
case "9RJh":
break;
case "9RJhl":
break;
case "9RJhl6":
break;
case "9RJhl6a":
break;
case "9RJhl6a0":
break;
case "9RJhl6a0n":
break;
default:
measureText.getText().clear();
break;
}
return;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
TODO Auto-generated method stub
return;
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
return;
}
});
Solution
I would say that the OnKeyUp
& OnKeyDown
event won’t cut it because soft keyboards hardly emit any of that. This is a rough prototype that filters the input of characters based on the expected string. There is a lot of room for improvement; While a custom implementation is still a better approach than trying to use the framework approach, the framework method may only capture the ⌫ key… FilteredEditText
captures any input before it may appear on the screen – in order to implement a keystroke pattern recorder, the expected string needs to be split into an ArrayList
, which will also save the duration between individual keystrokes; Once recorded, the collected information can be used for comparison.
/**
* Filtered {@link AppCompatEditText}
* @author Martin Zeitler
*/
public class FilteredEditText extends AppCompatEditText {
private static final String LOG_TAG = FilteredEditText.class.getSimpleName();
private String expectedString = null;
public FilteredEditText(Context context) {
super(context);
}
public FilteredEditText(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FilteredEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setExpectedString(@NonNull String value) {
this.expectedString = value;
this.setupInputFilter();
}
public void setupInputFilter() {
this.setFilters(new InputFilter[] {
new InputFilter() {
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int destStart, int destEnd) {
if (source.length() > 0 && source.charAt(end-1) == expectedString.charAt(destEnd)) {
/* valid input received */
Log.d(LOG_TAG, "input accepted: " + String.valueOf(source.charAt(end-1)));
return source;
} else {
/* invalid input received */
Log.d(LOG_TAG, "input rejected: " + String.valueOf(source.charAt(end-1)) + " - expected: " + String.valueOf(expectedString.charAt(destEnd)));
return "";
}
}
}
});
}
/** hardware event */
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
Log.d(LOG_TAG, "onKeyDown()");
return super.onKeyDown(keyCode, event);
}
/** hardware event */
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
Log.d(LOG_TAG, "onKeyUp()");
return super.onKeyUp(keyCode, event);
}
}
Usage example:
FilteredEditText mTextInput = findViewById(R.id.text_input);
mTextInput.setExpectedString("9RJhl6aH0n");
Logcat output:
D/FilteredEditText: input accepted: 9
D/FilteredEditText: input rejected: r - expected: R
D/FilteredEditText: input rejected: 4 - expected: R
D/FilteredEditText: input accepted: R
So far I have tested it with a software keyboard… And I can’t currently test it with a BT hardware keyboard because the battery is dead. I assume that InputFilter
captures all inputs.
Almost no OnKeyUp and OnKeyDown
software keyboard triggered events can be compensated for, because this still leads to a similar pattern when it is known when a keystroke is filtered – even if the duration of the keystroke cannot be measured, it is impossible to measure the speed of the keystroke’s attack because of the limitations of the software keyboard – the only possible workaround is to force the use of a hardware keyboard or create one, It emits these events for all keys (as opposed to the default
GBoard
, nor SwiftKey
). I’m just wondering now about swipe typing and voice typing… Because this is something that physical keystrokes dynamics hardly take into account. Even left feedback for GBoard
, as in some cases it would be helpful to issue a keycode optionally.
documentation also clearly states:
When handling keyboard events with the
KeyEvent
class and related APIs, you should expect that such keyboard events come only from a hardware keyboard. You should never rely on receiving key events for any key on a soft input method (an on-screen keyboard).
People can still use hardware events while using buttons to emit them; For example:
/**
* Fake Hardware {@link AppCompatButton}
* @see <a href="https://developer.android.com/reference/android/view/KeyEvent">KeyEvent</a>
* @author Martin Zeitler
*/
public class FakeHardwareButton extends AppCompatButton {
private BaseInputConnection mInputConnection;
private int keyCode = KeyEvent.KEYCODE_9;
private KeyEvent keyDown;
private KeyEvent keyUp;
public FakeHardwareButton(Context context) {
this(context, null);
}
public FakeHardwareButton(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FakeHardwareButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@SuppressLint("ClickableViewAccessibility")
private void setupInputConnection(View targetView) {
this.mInputConnection = new BaseInputConnection(targetView, true);
this.keyDown = new KeyEvent(KeyEvent.ACTION_DOWN, this.keyCode);
this.keyUp = new KeyEvent(KeyEvent.ACTION_UP, this.keyCode);
this.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
mInputConnection.sendKeyEvent(keyDown);
return true;
case MotionEvent.ACTION_UP:
mInputConnection.sendKeyEvent(keyUp);
return true;
}
return false;
}
});
}
}
The problem is just, for example. KeyEvent.KEYCODE_9
is not the same as KeyEvent.KEYCODE_NUMPAD_9
, so it is always necessary to compare the String
representation in the case of numeric keys.