Summary
In InputLogic.handleBackspaceEvent, the accelerated (held-backspace) second deletion measures its length from the first code point instead of the one it is actually about to delete. When the second character back is a multi-code-point emoji, this deletes the wrong number of chars and can split the emoji, leaving an orphaned surrogate or a stray variation selector / ZWJ.
Present at tag v4.1.2 (f0ff166ae).
Location
app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java, around lines 2019–2026:
final int codePointBeforeCursorToDeleteAgain = mConnection.getCodePointBeforeCursor();
if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) {
final int lengthToDeleteAgain = codePointBeforeCursor > 0xFE00 // <-- first code point
|| StringUtils.mightBeEmoji(codePointBeforeCursor) // <-- first code point
? mConnection.getCharCountToDeleteBeforeCursor()
: 1;
mConnection.deleteTextBeforeCursor(lengthToDeleteAgain);
totalDeletedLength += lengthToDeleteAgain;
}
The guard correctly re-reads the cursor into codePointBeforeCursorToDeleteAgain, but the ternary that decides the delete length still tests codePointBeforeCursor — the value from before the first deletion.
Consequences
Both directions are wrong:
- First char plain, second an emoji → the branch evaluates
false, so lengthToDeleteAgain is 1. A single char is removed from a multi-char emoji, splitting it.
- First char an emoji, second plain → the branch evaluates
true, so getCharCountToDeleteBeforeCursor() is used against a plain character and may remove more than intended.
Only reachable once mDeleteCount > Constants.DELETE_ACCELERATE_AT, i.e. while holding backspace, which is probably why it has gone unnoticed.
Suggested fix
Use the re-read value in the condition, so it describes the character actually being deleted:
final int lengthToDeleteAgain = codePointBeforeCursorToDeleteAgain > 0xFE00
|| StringUtils.mightBeEmoji(codePointBeforeCursorToDeleteAgain)
? mConnection.getCharCountToDeleteBeforeCursor()
: 1;
Notes
Found while merging v4.1.2 into a downstream fork. This almost certainly predates LeanType — the surrounding accelerated-delete code comes from the HeliBoard/AOSP lineage — so it may be worth forwarding to Helium314/HeliBoard as well.
I have no reproduction on a physical device; this is from reading the code, so please sanity-check the emoji-splitting claim against real input before acting on it.
Summary
In
InputLogic.handleBackspaceEvent, the accelerated (held-backspace) second deletion measures its length from the first code point instead of the one it is actually about to delete. When the second character back is a multi-code-point emoji, this deletes the wrong number ofchars and can split the emoji, leaving an orphaned surrogate or a stray variation selector / ZWJ.Present at tag v4.1.2 (
f0ff166ae).Location
app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java, around lines 2019–2026:The guard correctly re-reads the cursor into
codePointBeforeCursorToDeleteAgain, but the ternary that decides the delete length still testscodePointBeforeCursor— the value from before the first deletion.Consequences
Both directions are wrong:
false, solengthToDeleteAgainis1. A singlecharis removed from a multi-charemoji, splitting it.true, sogetCharCountToDeleteBeforeCursor()is used against a plain character and may remove more than intended.Only reachable once
mDeleteCount > Constants.DELETE_ACCELERATE_AT, i.e. while holding backspace, which is probably why it has gone unnoticed.Suggested fix
Use the re-read value in the condition, so it describes the character actually being deleted:
Notes
Found while merging v4.1.2 into a downstream fork. This almost certainly predates LeanType — the surrounding accelerated-delete code comes from the HeliBoard/AOSP lineage — so it may be worth forwarding to
Helium314/HeliBoardas well.I have no reproduction on a physical device; this is from reading the code, so please sanity-check the emoji-splitting claim against real input before acting on it.