Android keyboard remapping: AltGr+IJKL as arrow keys (with Ctrl combos)


March 4, 2026

Last updated: March 4, 2026

If you use a physical keyboard with your Android device, you've probably noticed there's no easy way to add custom key layers — like using AltGr+IJKL as arrow keys. On desktop Linux you have tools like Kanata or KMonad, but Android doesn't offer anything comparable out of the box.


In this post I'll show you how I got a full navigation layer working on Android — without root — by combining two tools:

  • exkeymo-server — generates an APK from a custom KCM (Key Character Map) file
  • Key Mapper — remaps keycodes using an AccessibilityService

Here's the full set of bindings:

ComboResult
AltGr + I / J / K / LUp / Left / Down / Right
AltGr + O / PBackspace / Delete
AltGr + H / ;Home / End
CapsLockEscape

All of the above also work with Shift (for text selection) and Ctrl (for word jumping / word selection). For example:

  • Shift+AltGr+J → Shift+Left (select one character left)
  • Ctrl+AltGr+J → Ctrl+Left (jump one word left)
  • Ctrl+Shift+AltGr+J → Ctrl+Shift+Left (select one word left)

  • An Android device with a physical keyboard connected (Bluetooth or USB)
  • exkeymo-server to generate the keyboard layout APK
  • Key Mapper installed from the Play Store or GitHub

Android uses Key Character Map (.kcm) files to define how physical scancodes map to characters and keycodes. With exkeymo-server, you can write a custom KCM file and generate an installable APK from it. After installing the APK, you go to Settings → System → Languages & input → Physical keyboard and select the exkeymo layout for your keyboard.

The KCM format supports a replace keyword that substitutes one keycode for another. This is the foundation of the navigation layer:

key J {
base: 'j'
shift, capslock: 'J'
ralt: replace DPAD_LEFT
shift+ralt: replace DPAD_LEFT
}

When you press AltGr+J, Android strips the AltGr modifier and emits a clean DPAD_LEFT event. Shift survives the replacement, so Shift+AltGr+J correctly produces Shift+DPAD_LEFT — text selection works as expected.


This is where things get tricky. You might expect ctrl+ralt: replace DPAD_LEFT to produce Ctrl+DPAD_LEFT, but it doesn't. The replace keyword strips all modifiers listed in the behavior's metaState — so both Ctrl and AltGr get removed. The app sees a plain DPAD_LEFT with no Ctrl modifier.

You might also hope that omitting the ctrl+ralt rule and relying on the ralt rule to match would work. It doesn't, because the AOSP matchesMetaState() function enforces exact matching for Ctrl, Alt, and Meta modifiers. If the pressed event has Ctrl bits that the matching rule doesn't account for, the match fails entirely.

Here's the relevant AOSP source (KeyCharacterMap.cpp):

bool KeyCharacterMap::matchesMetaState(int32_t eventMetaState,
int32_t behaviorMetaState) {
if ((eventMetaState & behaviorMetaState) == behaviorMetaState) {
const int32_t EXACT_META_STATES =
AMETA_CTRL_ON | AMETA_CTRL_LEFT_ON | AMETA_CTRL_RIGHT_ON
| AMETA_ALT_ON | AMETA_ALT_LEFT_ON | AMETA_ALT_RIGHT_ON
| AMETA_META_ON | AMETA_META_LEFT_ON | AMETA_META_RIGHT_ON;
int32_t unmatchedMetaState =
eventMetaState & ~behaviorMetaState & EXACT_META_STATES;
// ... cleanup logic ...
return !unmatchedMetaState;
}
return false;
}

And the replace stripping logic:

if (behavior->replacementKeyCode) {
toKeyCode = behavior->replacementKeyCode;
toMetaState = fromMetaState & ~behavior->metaState;
toMetaState = normalizeMetaState(toMetaState);
}

In short: the KCM format simply cannot produce Ctrl+Arrow from any AltGr-based combo. The fallback keyword doesn't help either — the original event gets consumed by the app (doing nothing), so the fallback never fires.


Since KCM files are a dead end for Ctrl combos, the workaround uses two layers:

  1. KCM file: Maps Ctrl+AltGr combos to unused sentinel keycodes (F13–F24, PROG_RED, PROG_GREEN) via replace. This strips all modifiers, producing a clean sentinel keycode event.
  2. Key Mapper: Catches the sentinel keycodes and emits the desired Ctrl+Arrow / Ctrl+Shift+Arrow combos.

SentinelSource comboKey Mapper target
F13Ctrl+AltGr+JCtrl+Left
F14Ctrl+AltGr+LCtrl+Right
F15Ctrl+AltGr+ICtrl+Up
F16Ctrl+AltGr+KCtrl+Down
F17Ctrl+AltGr+HCtrl+Home
F18Ctrl+AltGr+;Ctrl+End
F19Ctrl+Shift+AltGr+JCtrl+Shift+Left
F20Ctrl+Shift+AltGr+LCtrl+Shift+Right
F21Ctrl+Shift+AltGr+ICtrl+Shift+Up
F22Ctrl+Shift+AltGr+KCtrl+Shift+Down
F23Ctrl+Shift+AltGr+HCtrl+Shift+Home
F24Ctrl+Shift+AltGr+;Ctrl+Shift+End
PROG_REDCtrl+AltGr+OCtrl+Backspace
PROG_GREENCtrl+AltGr+PCtrl+Delete

type OVERLAY
# CapsLock → Escape
map key 58 ESCAPE
key J {
base: 'j'
shift, capslock: 'J'
ralt: replace DPAD_LEFT
shift+ralt: replace DPAD_LEFT
ctrl+ralt: replace F13
ctrl+shift+ralt: replace F19
}
key L {
base: 'l'
shift, capslock: 'L'
ralt: replace DPAD_RIGHT
shift+ralt: replace DPAD_RIGHT
ctrl+ralt: replace F14
ctrl+shift+ralt: replace F20
}
key I {
base: 'i'
shift, capslock: 'I'
ralt: replace DPAD_UP
shift+ralt: replace DPAD_UP
ctrl+ralt: replace F15
ctrl+shift+ralt: replace F21
}
key K {
base: 'k'
shift, capslock: 'K'
ralt: replace DPAD_DOWN
shift+ralt: replace DPAD_DOWN
ctrl+ralt: replace F16
ctrl+shift+ralt: replace F22
}
key O {
base: 'o'
shift, capslock: 'O'
ralt: replace DEL
shift+ralt: replace DEL
ctrl+ralt: replace PROG_RED
}
key P {
base: 'p'
shift, capslock: 'P'
ralt: replace FORWARD_DEL
shift+ralt: replace FORWARD_DEL
ctrl+ralt: replace PROG_GREEN
}
key H {
base: 'h'
shift, capslock: 'H'
ralt: replace MOVE_HOME
shift+ralt: replace MOVE_HOME
ctrl+ralt: replace F17
ctrl+shift+ralt: replace F23
}
key SEMICOLON {
base: ';'
shift: ':'
ralt: replace MOVE_END
shift+ralt: replace MOVE_END
ctrl+ralt: replace F18
ctrl+shift+ralt: replace F24
}

Save this as a .kcm file and feed it to exkeymo-server to generate an APK. Install the APK, then go to Settings → System → Languages & input → Physical keyboard and select the exkeymo layout for your connected keyboard.


Install Key Mapper and grant it Accessibility Service permissions when prompted.

For each of the 14 sentinel mappings, you need to create a trigger → action pair. Here's the process:

Since keys like F13 or PROG_RED don't physically exist on your keyboard, you might wonder how to record them as triggers. The trick is that the KCM file is already active — so when you press Ctrl+AltGr+J during Key Mapper's "record trigger" mode, the KCM replaces it with F13 before Key Mapper sees it. Key Mapper records F13 as the trigger.

However, Key Mapper will also record all the modifier keys you held down (Ctrl, Shift, AltGr). You need to remove these extra modifier keys from the recorded trigger, leaving only the sentinel key (e.g. just F13). This is important — if you leave the modifiers in, the trigger won't match because replace already stripped them at the KCM level.

For each trigger, create a "Key Event" action with the target keycode and modifiers:

Trigger (sentinel only)Action keycodeAction modifiers
F13DPAD_LEFT (21)Ctrl
F14DPAD_RIGHT (22)Ctrl
F15DPAD_UP (19)Ctrl
F16DPAD_DOWN (20)Ctrl
F17MOVE_HOME (122)Ctrl
F18MOVE_END (123)Ctrl
F19DPAD_LEFT (21)Ctrl + Shift
F20DPAD_RIGHT (22)Ctrl + Shift
F21DPAD_UP (19)Ctrl + Shift
F22DPAD_DOWN (20)Ctrl + Shift
F23MOVE_HOME (122)Ctrl + Shift
F24MOVE_END (123)Ctrl + Shift
PROG_RED (216)DEL (67)Ctrl
PROG_GREEN (217)FORWARD_DEL (112)Ctrl

The plain AltGr combos and Shift+AltGr combos are handled entirely by the KCM file — no extra app needed. For Ctrl and Ctrl+Shift combos, the KCM converts them to sentinel keycodes (F13–F24, PROG_RED, PROG_GREEN), and Key Mapper translates those sentinels into the final Ctrl+Arrow / Ctrl+Shift+Arrow events.

It's a two-layer workaround for a limitation in Android's KCM replace logic, but once set up, it works reliably and doesn't require root. You get a full Vim-style navigation layer on Android with proper word jumping and text selection support.