KeyGlyphMap API implementation

DD: go/pk_glyph_map
PRD doc: go/pk_glyph_map_prd
Test: atest KeyboardGlyphManagerTests
Bug: 345440920
Flag: com.android.hardware.input.keyboard_glyph_map

Change-Id: I5aec1bf2c7369379f1a384efd7ebd5d1a25db93a
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index 5e46ae0..23e262c 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -159,6 +159,34 @@
             "android.hardware.input.metadata.KEYBOARD_LAYOUTS";
 
     /**
+     * Broadcast Action: Query available keyboard glyph maps.
+     * <p>
+     * The input manager service locates available keyboard glyph maps
+     * by querying broadcast receivers that are registered for this action.
+     * An application can offer additional keyboard glyph maps to the user
+     * by declaring a suitable broadcast receiver in its manifest.
+     * </p>
+     *
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_QUERY_KEYBOARD_GLYPH_MAPS =
+            "android.hardware.input.action.QUERY_KEYBOARD_GLYPH_MAPS";
+
+    /**
+     * Metadata Key: Keyboard glyph map metadata associated with
+     * {@link #ACTION_QUERY_KEYBOARD_GLYPH_MAPS}.
+     * <p>
+     * Specifies the resource id of a XML resource that describes the keyboard
+     * glyph maps that are provided by the application.
+     * </p>
+     *
+     * @hide
+     */
+    public static final String META_DATA_KEYBOARD_GLYPH_MAPS =
+            "android.hardware.input.metadata.KEYBOARD_GLYPH_MAPS";
+
+    /**
      * Prevent touches from being consumed by apps if these touches passed through a non-trusted
      * window from a different UID and are considered unsafe.
      *
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 4883827..ea993ee 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -2184,6 +2184,344 @@
         <enum name="KEYCODE_DEMO_APP_2" value="302" />
         <enum name="KEYCODE_DEMO_APP_3" value="303" />
         <enum name="KEYCODE_DEMO_APP_4" value="304" />
+        <enum name="KEYCODE_KEYBOARD_BACKLIGHT_DOWN" value="305" />
+        <enum name="KEYCODE_KEYBOARD_BACKLIGHT_UP" value="306" />
+        <enum name="KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE" value="307" />
+        <enum name="KEYCODE_STYLUS_BUTTON_PRIMARY" value="308" />
+        <enum name="KEYCODE_STYLUS_BUTTON_SECONDARY" value="309" />
+        <enum name="KEYCODE_STYLUS_BUTTON_TERTIARY" value="310" />
+        <enum name="KEYCODE_STYLUS_BUTTON_TAIL" value="311" />
+        <enum name="KEYCODE_RECENT_APPS" value="312" />
+        <enum name="KEYCODE_MACRO_1" value="313" />
+        <enum name="KEYCODE_MACRO_2" value="314" />
+        <enum name="KEYCODE_MACRO_3" value="315" />
+        <enum name="KEYCODE_MACRO_4" value="316" />
+        <enum name="KEYCODE_EMOJI_PICKER" value="317" />
+        <enum name="KEYCODE_SCREENSHOT" value="318" />
+    </attr>
+
+    <!-- @hide same as keycode enum defined above, but to be used to define keycode output.
+         (redefining it to allow keycode and outKeycode to be part of same styleable attribute) -->
+    <attr name="outKeycode">
+        <enum name="KEYCODE_UNKNOWN" value="0" />
+        <enum name="KEYCODE_SOFT_LEFT" value="1" />
+        <enum name="KEYCODE_SOFT_RIGHT" value="2" />
+        <enum name="KEYCODE_HOME" value="3" />
+        <enum name="KEYCODE_BACK" value="4" />
+        <enum name="KEYCODE_CALL" value="5" />
+        <enum name="KEYCODE_ENDCALL" value="6" />
+        <enum name="KEYCODE_0" value="7" />
+        <enum name="KEYCODE_1" value="8" />
+        <enum name="KEYCODE_2" value="9" />
+        <enum name="KEYCODE_3" value="10" />
+        <enum name="KEYCODE_4" value="11" />
+        <enum name="KEYCODE_5" value="12" />
+        <enum name="KEYCODE_6" value="13" />
+        <enum name="KEYCODE_7" value="14" />
+        <enum name="KEYCODE_8" value="15" />
+        <enum name="KEYCODE_9" value="16" />
+        <enum name="KEYCODE_STAR" value="17" />
+        <enum name="KEYCODE_POUND" value="18" />
+        <enum name="KEYCODE_DPAD_UP" value="19" />
+        <enum name="KEYCODE_DPAD_DOWN" value="20" />
+        <enum name="KEYCODE_DPAD_LEFT" value="21" />
+        <enum name="KEYCODE_DPAD_RIGHT" value="22" />
+        <enum name="KEYCODE_DPAD_CENTER" value="23" />
+        <enum name="KEYCODE_VOLUME_UP" value="24" />
+        <enum name="KEYCODE_VOLUME_DOWN" value="25" />
+        <enum name="KEYCODE_POWER" value="26" />
+        <enum name="KEYCODE_CAMERA" value="27" />
+        <enum name="KEYCODE_CLEAR" value="28" />
+        <enum name="KEYCODE_A" value="29" />
+        <enum name="KEYCODE_B" value="30" />
+        <enum name="KEYCODE_C" value="31" />
+        <enum name="KEYCODE_D" value="32" />
+        <enum name="KEYCODE_E" value="33" />
+        <enum name="KEYCODE_F" value="34" />
+        <enum name="KEYCODE_G" value="35" />
+        <enum name="KEYCODE_H" value="36" />
+        <enum name="KEYCODE_I" value="37" />
+        <enum name="KEYCODE_J" value="38" />
+        <enum name="KEYCODE_K" value="39" />
+        <enum name="KEYCODE_L" value="40" />
+        <enum name="KEYCODE_M" value="41" />
+        <enum name="KEYCODE_N" value="42" />
+        <enum name="KEYCODE_O" value="43" />
+        <enum name="KEYCODE_P" value="44" />
+        <enum name="KEYCODE_Q" value="45" />
+        <enum name="KEYCODE_R" value="46" />
+        <enum name="KEYCODE_S" value="47" />
+        <enum name="KEYCODE_T" value="48" />
+        <enum name="KEYCODE_U" value="49" />
+        <enum name="KEYCODE_V" value="50" />
+        <enum name="KEYCODE_W" value="51" />
+        <enum name="KEYCODE_X" value="52" />
+        <enum name="KEYCODE_Y" value="53" />
+        <enum name="KEYCODE_Z" value="54" />
+        <enum name="KEYCODE_COMMA" value="55" />
+        <enum name="KEYCODE_PERIOD" value="56" />
+        <enum name="KEYCODE_ALT_LEFT" value="57" />
+        <enum name="KEYCODE_ALT_RIGHT" value="58" />
+        <enum name="KEYCODE_SHIFT_LEFT" value="59" />
+        <enum name="KEYCODE_SHIFT_RIGHT" value="60" />
+        <enum name="KEYCODE_TAB" value="61" />
+        <enum name="KEYCODE_SPACE" value="62" />
+        <enum name="KEYCODE_SYM" value="63" />
+        <enum name="KEYCODE_EXPLORER" value="64" />
+        <enum name="KEYCODE_ENVELOPE" value="65" />
+        <enum name="KEYCODE_ENTER" value="66" />
+        <enum name="KEYCODE_DEL" value="67" />
+        <enum name="KEYCODE_GRAVE" value="68" />
+        <enum name="KEYCODE_MINUS" value="69" />
+        <enum name="KEYCODE_EQUALS" value="70" />
+        <enum name="KEYCODE_LEFT_BRACKET" value="71" />
+        <enum name="KEYCODE_RIGHT_BRACKET" value="72" />
+        <enum name="KEYCODE_BACKSLASH" value="73" />
+        <enum name="KEYCODE_SEMICOLON" value="74" />
+        <enum name="KEYCODE_APOSTROPHE" value="75" />
+        <enum name="KEYCODE_SLASH" value="76" />
+        <enum name="KEYCODE_AT" value="77" />
+        <enum name="KEYCODE_NUM" value="78" />
+        <enum name="KEYCODE_HEADSETHOOK" value="79" />
+        <enum name="KEYCODE_FOCUS" value="80" />
+        <enum name="KEYCODE_PLUS" value="81" />
+        <enum name="KEYCODE_MENU" value="82" />
+        <enum name="KEYCODE_NOTIFICATION" value="83" />
+        <enum name="KEYCODE_SEARCH" value="84" />
+        <enum name="KEYCODE_MEDIA_PLAY_PAUSE" value="85" />
+        <enum name="KEYCODE_MEDIA_STOP" value="86" />
+        <enum name="KEYCODE_MEDIA_NEXT" value="87" />
+        <enum name="KEYCODE_MEDIA_PREVIOUS" value="88" />
+        <enum name="KEYCODE_MEDIA_REWIND" value="89" />
+        <enum name="KEYCODE_MEDIA_FAST_FORWARD" value="90" />
+        <enum name="KEYCODE_MUTE" value="91" />
+        <enum name="KEYCODE_PAGE_UP" value="92" />
+        <enum name="KEYCODE_PAGE_DOWN" value="93" />
+        <enum name="KEYCODE_PICTSYMBOLS" value="94" />
+        <enum name="KEYCODE_SWITCH_CHARSET" value="95" />
+        <enum name="KEYCODE_BUTTON_A" value="96" />
+        <enum name="KEYCODE_BUTTON_B" value="97" />
+        <enum name="KEYCODE_BUTTON_C" value="98" />
+        <enum name="KEYCODE_BUTTON_X" value="99" />
+        <enum name="KEYCODE_BUTTON_Y" value="100" />
+        <enum name="KEYCODE_BUTTON_Z" value="101" />
+        <enum name="KEYCODE_BUTTON_L1" value="102" />
+        <enum name="KEYCODE_BUTTON_R1" value="103" />
+        <enum name="KEYCODE_BUTTON_L2" value="104" />
+        <enum name="KEYCODE_BUTTON_R2" value="105" />
+        <enum name="KEYCODE_BUTTON_THUMBL" value="106" />
+        <enum name="KEYCODE_BUTTON_THUMBR" value="107" />
+        <enum name="KEYCODE_BUTTON_START" value="108" />
+        <enum name="KEYCODE_BUTTON_SELECT" value="109" />
+        <enum name="KEYCODE_BUTTON_MODE" value="110" />
+        <enum name="KEYCODE_ESCAPE" value="111" />
+        <enum name="KEYCODE_FORWARD_DEL" value="112" />
+        <enum name="KEYCODE_CTRL_LEFT" value="113" />
+        <enum name="KEYCODE_CTRL_RIGHT" value="114" />
+        <enum name="KEYCODE_CAPS_LOCK" value="115" />
+        <enum name="KEYCODE_SCROLL_LOCK" value="116" />
+        <enum name="KEYCODE_META_LEFT" value="117" />
+        <enum name="KEYCODE_META_RIGHT" value="118" />
+        <enum name="KEYCODE_FUNCTION" value="119" />
+        <enum name="KEYCODE_SYSRQ" value="120" />
+        <enum name="KEYCODE_BREAK" value="121" />
+        <enum name="KEYCODE_MOVE_HOME" value="122" />
+        <enum name="KEYCODE_MOVE_END" value="123" />
+        <enum name="KEYCODE_INSERT" value="124" />
+        <enum name="KEYCODE_FORWARD" value="125" />
+        <enum name="KEYCODE_MEDIA_PLAY" value="126" />
+        <enum name="KEYCODE_MEDIA_PAUSE" value="127" />
+        <enum name="KEYCODE_MEDIA_CLOSE" value="128" />
+        <enum name="KEYCODE_MEDIA_EJECT" value="129" />
+        <enum name="KEYCODE_MEDIA_RECORD" value="130" />
+        <enum name="KEYCODE_F1" value="131" />
+        <enum name="KEYCODE_F2" value="132" />
+        <enum name="KEYCODE_F3" value="133" />
+        <enum name="KEYCODE_F4" value="134" />
+        <enum name="KEYCODE_F5" value="135" />
+        <enum name="KEYCODE_F6" value="136" />
+        <enum name="KEYCODE_F7" value="137" />
+        <enum name="KEYCODE_F8" value="138" />
+        <enum name="KEYCODE_F9" value="139" />
+        <enum name="KEYCODE_F10" value="140" />
+        <enum name="KEYCODE_F11" value="141" />
+        <enum name="KEYCODE_F12" value="142" />
+        <enum name="KEYCODE_NUM_LOCK" value="143" />
+        <enum name="KEYCODE_NUMPAD_0" value="144" />
+        <enum name="KEYCODE_NUMPAD_1" value="145" />
+        <enum name="KEYCODE_NUMPAD_2" value="146" />
+        <enum name="KEYCODE_NUMPAD_3" value="147" />
+        <enum name="KEYCODE_NUMPAD_4" value="148" />
+        <enum name="KEYCODE_NUMPAD_5" value="149" />
+        <enum name="KEYCODE_NUMPAD_6" value="150" />
+        <enum name="KEYCODE_NUMPAD_7" value="151" />
+        <enum name="KEYCODE_NUMPAD_8" value="152" />
+        <enum name="KEYCODE_NUMPAD_9" value="153" />
+        <enum name="KEYCODE_NUMPAD_DIVIDE" value="154" />
+        <enum name="KEYCODE_NUMPAD_MULTIPLY" value="155" />
+        <enum name="KEYCODE_NUMPAD_SUBTRACT" value="156" />
+        <enum name="KEYCODE_NUMPAD_ADD" value="157" />
+        <enum name="KEYCODE_NUMPAD_DOT" value="158" />
+        <enum name="KEYCODE_NUMPAD_COMMA" value="159" />
+        <enum name="KEYCODE_NUMPAD_ENTER" value="160" />
+        <enum name="KEYCODE_NUMPAD_EQUALS" value="161" />
+        <enum name="KEYCODE_NUMPAD_LEFT_PAREN" value="162" />
+        <enum name="KEYCODE_NUMPAD_RIGHT_PAREN" value="163" />
+        <enum name="KEYCODE_VOLUME_MUTE" value="164" />
+        <enum name="KEYCODE_INFO" value="165" />
+        <enum name="KEYCODE_CHANNEL_UP" value="166" />
+        <enum name="KEYCODE_CHANNEL_DOWN" value="167" />
+        <enum name="KEYCODE_ZOOM_IN" value="168" />
+        <enum name="KEYCODE_ZOOM_OUT" value="169" />
+        <enum name="KEYCODE_TV" value="170" />
+        <enum name="KEYCODE_WINDOW" value="171" />
+        <enum name="KEYCODE_GUIDE" value="172" />
+        <enum name="KEYCODE_DVR" value="173" />
+        <enum name="KEYCODE_BOOKMARK" value="174" />
+        <enum name="KEYCODE_CAPTIONS" value="175" />
+        <enum name="KEYCODE_SETTINGS" value="176" />
+        <enum name="KEYCODE_TV_POWER" value="177" />
+        <enum name="KEYCODE_TV_INPUT" value="178" />
+        <enum name="KEYCODE_STB_POWER" value="179" />
+        <enum name="KEYCODE_STB_INPUT" value="180" />
+        <enum name="KEYCODE_AVR_POWER" value="181" />
+        <enum name="KEYCODE_AVR_INPUT" value="182" />
+        <enum name="KEYCODE_PROG_GRED" value="183" />
+        <enum name="KEYCODE_PROG_GREEN" value="184" />
+        <enum name="KEYCODE_PROG_YELLOW" value="185" />
+        <enum name="KEYCODE_PROG_BLUE" value="186" />
+        <enum name="KEYCODE_APP_SWITCH" value="187" />
+        <enum name="KEYCODE_BUTTON_1" value="188" />
+        <enum name="KEYCODE_BUTTON_2" value="189" />
+        <enum name="KEYCODE_BUTTON_3" value="190" />
+        <enum name="KEYCODE_BUTTON_4" value="191" />
+        <enum name="KEYCODE_BUTTON_5" value="192" />
+        <enum name="KEYCODE_BUTTON_6" value="193" />
+        <enum name="KEYCODE_BUTTON_7" value="194" />
+        <enum name="KEYCODE_BUTTON_8" value="195" />
+        <enum name="KEYCODE_BUTTON_9" value="196" />
+        <enum name="KEYCODE_BUTTON_10" value="197" />
+        <enum name="KEYCODE_BUTTON_11" value="198" />
+        <enum name="KEYCODE_BUTTON_12" value="199" />
+        <enum name="KEYCODE_BUTTON_13" value="200" />
+        <enum name="KEYCODE_BUTTON_14" value="201" />
+        <enum name="KEYCODE_BUTTON_15" value="202" />
+        <enum name="KEYCODE_BUTTON_16" value="203" />
+        <enum name="KEYCODE_LANGUAGE_SWITCH" value="204" />
+        <enum name="KEYCODE_MANNER_MODE" value="205" />
+        <enum name="KEYCODE_3D_MODE" value="206" />
+        <enum name="KEYCODE_CONTACTS" value="207" />
+        <enum name="KEYCODE_CALENDAR" value="208" />
+        <enum name="KEYCODE_MUSIC" value="209" />
+        <enum name="KEYCODE_CALCULATOR" value="210" />
+        <enum name="KEYCODE_ZENKAKU_HANKAKU" value="211" />
+        <enum name="KEYCODE_EISU" value="212" />
+        <enum name="KEYCODE_MUHENKAN" value="213" />
+        <enum name="KEYCODE_HENKAN" value="214" />
+        <enum name="KEYCODE_KATAKANA_HIRAGANA" value="215" />
+        <enum name="KEYCODE_YEN" value="216" />
+        <enum name="KEYCODE_RO" value="217" />
+        <enum name="KEYCODE_KANA" value="218" />
+        <enum name="KEYCODE_ASSIST" value="219" />
+        <enum name="KEYCODE_BRIGHTNESS_DOWN" value="220" />
+        <enum name="KEYCODE_BRIGHTNESS_UP" value="221" />
+        <enum name="KEYCODE_MEDIA_AUDIO_TRACK" value="222" />
+        <enum name="KEYCODE_MEDIA_SLEEP" value="223" />
+        <enum name="KEYCODE_MEDIA_WAKEUP" value="224" />
+        <enum name="KEYCODE_PAIRING" value="225" />
+        <enum name="KEYCODE_MEDIA_TOP_MENU" value="226" />
+        <enum name="KEYCODE_11" value="227" />
+        <enum name="KEYCODE_12" value="228" />
+        <enum name="KEYCODE_LAST_CHANNEL" value="229" />
+        <enum name="KEYCODE_TV_DATA_SERVICE" value="230" />
+        <enum name="KEYCODE_VOICE_ASSIST" value="231" />
+        <enum name="KEYCODE_TV_RADIO_SERVICE" value="232" />
+        <enum name="KEYCODE_TV_TELETEXT" value="233" />
+        <enum name="KEYCODE_TV_NUMBER_ENTRY" value="234" />
+        <enum name="KEYCODE_TV_TERRESTRIAL_ANALOG" value="235" />
+        <enum name="KEYCODE_TV_TERRESTRIAL_DIGITAL" value="236" />
+        <enum name="KEYCODE_TV_SATELLITE" value="237" />
+        <enum name="KEYCODE_TV_SATELLITE_BS" value="238" />
+        <enum name="KEYCODE_TV_SATELLITE_CS" value="239" />
+        <enum name="KEYCODE_TV_SATELLITE_SERVICE" value="240" />
+        <enum name="KEYCODE_TV_NETWORK" value="241" />
+        <enum name="KEYCODE_TV_ANTENNA_CABLE" value="242" />
+        <enum name="KEYCODE_TV_INPUT_HDMI_1" value="243" />
+        <enum name="KEYCODE_TV_INPUT_HDMI_2" value="244" />
+        <enum name="KEYCODE_TV_INPUT_HDMI_3" value="245" />
+        <enum name="KEYCODE_TV_INPUT_HDMI_4" value="246" />
+        <enum name="KEYCODE_TV_INPUT_COMPOSITE_1" value="247" />
+        <enum name="KEYCODE_TV_INPUT_COMPOSITE_2" value="248" />
+        <enum name="KEYCODE_TV_INPUT_COMPONENT_1" value="249" />
+        <enum name="KEYCODE_TV_INPUT_COMPONENT_2" value="250" />
+        <enum name="KEYCODE_TV_INPUT_VGA_1" value="251" />
+        <enum name="KEYCODE_TV_AUDIO_DESCRIPTION" value="252" />
+        <enum name="KEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP" value="253" />
+        <enum name="KEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN" value="254" />
+        <enum name="KEYCODE_TV_ZOOM_MODE" value="255" />
+        <enum name="KEYCODE_TV_CONTENTS_MENU" value="256" />
+        <enum name="KEYCODE_TV_MEDIA_CONTEXT_MENU" value="257" />
+        <enum name="KEYCODE_TV_TIMER_PROGRAMMING" value="258" />
+        <enum name="KEYCODE_HELP" value="259" />
+        <enum name="KEYCODE_NAVIGATE_PREVIOUS" value="260" />
+        <enum name="KEYCODE_NAVIGATE_NEXT" value="261" />
+        <enum name="KEYCODE_NAVIGATE_IN" value="262" />
+        <enum name="KEYCODE_NAVIGATE_OUT" value="263" />
+        <enum name="KEYCODE_STEM_PRIMARY" value="264" />
+        <enum name="KEYCODE_STEM_1" value="265" />
+        <enum name="KEYCODE_STEM_2" value="266" />
+        <enum name="KEYCODE_STEM_3" value="267" />
+        <enum name="KEYCODE_DPAD_UP_LEFT" value="268" />
+        <enum name="KEYCODE_DPAD_DOWN_LEFT" value="269" />
+        <enum name="KEYCODE_DPAD_UP_RIGHT" value="270" />
+        <enum name="KEYCODE_DPAD_DOWN_RIGHT" value="271" />
+        <enum name="KEYCODE_MEDIA_SKIP_FORWARD" value="272" />
+        <enum name="KEYCODE_MEDIA_SKIP_BACKWARD" value="273" />
+        <enum name="KEYCODE_MEDIA_STEP_FORWARD" value="274" />
+        <enum name="KEYCODE_MEDIA_STEP_BACKWARD" value="275" />
+        <enum name="KEYCODE_SOFT_SLEEP" value="276" />
+        <enum name="KEYCODE_CUT" value="277" />
+        <enum name="KEYCODE_COPY" value="278" />
+        <enum name="KEYCODE_PASTE" value="279" />
+        <enum name="KEYCODE_SYSTEM_NAVIGATION_UP" value="280" />
+        <enum name="KEYCODE_SYSTEM_NAVIGATION_DOWN" value="281" />
+        <enum name="KEYCODE_SYSTEM_NAVIGATION_LEFT" value="282" />
+        <enum name="KEYCODE_SYSTEM_NAVIGATION_RIGHT" value="283" />
+        <enum name="KEYCODE_ALL_APPS" value="284" />
+        <enum name="KEYCODE_REFRESH" value="285" />
+        <enum name="KEYCODE_THUMBS_UP" value="286" />
+        <enum name="KEYCODE_THUMBS_DOWN" value="287" />
+        <enum name="KEYCODE_PROFILE_SWITCH" value="288" />
+        <enum name="KEYCODE_VIDEO_APP_1" value="289" />
+        <enum name="KEYCODE_VIDEO_APP_2" value="290" />
+        <enum name="KEYCODE_VIDEO_APP_3" value="291" />
+        <enum name="KEYCODE_VIDEO_APP_4" value="292" />
+        <enum name="KEYCODE_VIDEO_APP_5" value="293" />
+        <enum name="KEYCODE_VIDEO_APP_6" value="294" />
+        <enum name="KEYCODE_VIDEO_APP_7" value="295" />
+        <enum name="KEYCODE_VIDEO_APP_8" value="296" />
+        <enum name="KEYCODE_FEATURED_APP_1" value="297" />
+        <enum name="KEYCODE_FEATURED_APP_2" value="298" />
+        <enum name="KEYCODE_FEATURED_APP_3" value="299" />
+        <enum name="KEYCODE_FEATURED_APP_4" value="300" />
+        <enum name="KEYCODE_DEMO_APP_1" value="301" />
+        <enum name="KEYCODE_DEMO_APP_2" value="302" />
+        <enum name="KEYCODE_DEMO_APP_3" value="303" />
+        <enum name="KEYCODE_DEMO_APP_4" value="304" />
+        <enum name="KEYCODE_KEYBOARD_BACKLIGHT_DOWN" value="305" />
+        <enum name="KEYCODE_KEYBOARD_BACKLIGHT_UP" value="306" />
+        <enum name="KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE" value="307" />
+        <enum name="KEYCODE_STYLUS_BUTTON_PRIMARY" value="308" />
+        <enum name="KEYCODE_STYLUS_BUTTON_SECONDARY" value="309" />
+        <enum name="KEYCODE_STYLUS_BUTTON_TERTIARY" value="310" />
+        <enum name="KEYCODE_STYLUS_BUTTON_TAIL" value="311" />
+        <enum name="KEYCODE_RECENT_APPS" value="312" />
+        <enum name="KEYCODE_MACRO_1" value="313" />
+        <enum name="KEYCODE_MACRO_2" value="314" />
+        <enum name="KEYCODE_MACRO_3" value="315" />
+        <enum name="KEYCODE_MACRO_4" value="316" />
+        <enum name="KEYCODE_EMOJI_PICKER" value="317" />
+        <enum name="KEYCODE_SCREENSHOT" value="318" />
     </attr>
 
     <!-- ***************************************************************** -->
@@ -9844,6 +10182,63 @@
         <attr name="productId" format="integer" />
     </declare-styleable>
 
+    <!-- @hide -->
+    <declare-styleable name="KeyboardGlyphMap">
+        <attr name="glyphMap" format="reference" />
+        <attr name="vendorId" format="integer" />
+        <attr name="productId" format="integer" />
+    </declare-styleable>
+
+    <!-- @hide -->
+    <declare-styleable name="KeyGlyph">
+        <attr name="keycode" />
+        <attr name="glyphDrawable" format="reference" />
+    </declare-styleable>
+
+    <!-- @hide -->
+    <declare-styleable name="ModifierGlyph">
+        <!-- The values are taken from public constants for modifier state defined in
+             {@see KeyEvent.java}. Here we explicitly allow only one modifier bit as value, since
+             this represents the modifier key -->
+        <attr name="modifier">
+            <enum name="META" value="0x10000" />
+            <enum name="CTRL" value="0x1000" />
+            <enum name="ALT" value="0x02" />
+            <enum name="SHIFT" value="0x1" />
+            <enum name="SYM" value="0x4" />
+            <enum name="FUNCTION" value="0x8" />
+            <enum name="CAPS_LOCK" value="0x100000" />
+            <enum name="NUM_LOCK" value="0x200000" />
+            <enum name="SCROLL_LOCK" value="0x400000" />
+        </attr>
+        <attr name="glyphDrawable" format="reference" />
+    </declare-styleable>
+
+    <!-- @hide -->
+    <declare-styleable name="HardwareDefinedShortcut">
+        <attr name="keycode" />
+        <!-- The values are taken from public constants for modifier state defined in
+             {@see KeyEvent.java}. Here we allow multiple modifier flags as value, since this
+             represents the modifier state -->
+        <attr name="modifierState">
+            <flag name="META" value="0x10000" />
+            <flag name="CTRL" value="0x1000" />
+            <flag name="ALT" value="0x02" />
+            <flag name="SHIFT" value="0x1" />
+            <flag name="SYM" value="0x4" />
+            <flag name="FUNCTION" value="0x8" />
+            <flag name="CAPS_LOCK" value="0x100000" />
+            <flag name="NUM_LOCK" value="0x200000" />
+            <flag name="SCROLL_LOCK" value="0x400000" />
+        </attr>
+        <attr name="outKeycode" />
+    </declare-styleable>
+
+    <!-- @hide -->
+    <declare-styleable name="FunctionRowKey">
+        <attr name="keycode" />
+    </declare-styleable>
+
     <declare-styleable name="MediaRouteButton">
         <!-- This drawable is a state list where the "activated" state
              indicates active media routing. Non-activated indicates
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index b23546c..bef984b 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -312,6 +312,9 @@
     // Manages Keyboard modifier keys remapping
     private final KeyRemapper mKeyRemapper;
 
+    // Manages Keyboard glyphs for specific keyboards
+    private final KeyboardGlyphManager mKeyboardGlyphManager;
+
     // Manages loading PointerIcons
     private final PointerIconCache mPointerIconCache;
 
@@ -461,6 +464,7 @@
         mKeyboardLedController = new KeyboardLedController(mContext, injector.getLooper(),
                 mNative);
         mKeyRemapper = new KeyRemapper(mContext, mNative, mDataStore, injector.getLooper());
+        mKeyboardGlyphManager = new KeyboardGlyphManager(mContext, injector.getLooper());
         mPointerIconCache = new PointerIconCache(mContext, mNative);
 
         mUseDevInputEventForAudioJack =
@@ -577,6 +581,7 @@
         mKeyboardLedController.systemRunning();
         mKeyRemapper.systemRunning();
         mPointerIconCache.systemRunning();
+        mKeyboardGlyphManager.systemRunning();
     }
 
     private void reloadDeviceAliases() {
@@ -1211,8 +1216,7 @@
 
     @Override // Binder call
     public KeyGlyphMap getKeyGlyphMap(int deviceId) {
-        // TODO(b/345440920): Implementation
-        return null;
+        return mKeyboardGlyphManager.getKeyGlyphMap(deviceId);
     }
 
     public void setFocusedApplication(int displayId, InputApplicationHandle application) {
@@ -2084,6 +2088,7 @@
         mBatteryController.dump(ipw);
         mKeyboardBacklightController.dump(ipw);
         mKeyboardLedController.dump(ipw);
+        mKeyboardGlyphManager.dump(ipw);
     }
 
     private void dumpAssociations(IndentingPrintWriter pw) {
diff --git a/services/core/java/com/android/server/input/KeyboardGlyphManager.java b/services/core/java/com/android/server/input/KeyboardGlyphManager.java
new file mode 100644
index 0000000..f59d72b
--- /dev/null
+++ b/services/core/java/com/android/server/input/KeyboardGlyphManager.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.input;
+
+import static com.android.hardware.input.Flags.keyboardGlyphMap;
+
+import android.annotation.AnyRes;
+import android.annotation.MainThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.hardware.input.InputManager;
+import android.hardware.input.KeyGlyphMap;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.view.InputDevice;
+
+import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Provides information on custom glyphs configured for specific keyboard devices.
+ *
+ * @hide
+ */
+public final class KeyboardGlyphManager implements InputManager.InputDeviceListener {
+
+    private static final String TAG = "KeyboardGlyphManager";
+    // To enable these logs, run: 'adb shell setprop log.tag.KeyboardGlyphManager DEBUG'
+    // (requires restart)
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private static final String TAG_KEYBOARD_GLYPH_MAPS = "keyboard-glyph-maps";
+    private static final String TAG_KEYBOARD_GLYPH_MAP = "keyboard-glyph-map";
+    private static final String TAG_KEY_GLYPH = "key-glyph";
+    private static final String TAG_MODIFIER_GLYPH = "modifier-glyph";
+    private static final String TAG_FUNCTION_ROW_KEY = "function-row-key";
+    private static final String TAG_HARDWARE_DEFINED_SHORTCUT = "hardware-defined-shortcut";
+
+    private final Context mContext;
+    private final Handler mHandler;
+    private final Object mGlyphMapLock = new Object();
+    @GuardedBy("mGlyphMapLock")
+    private boolean mGlyphMapDataLoaded = false;
+    @GuardedBy("mGlyphMapLock")
+    private List<KeyGlyphMapData> mGlyphMapDataList = new ArrayList<>();
+    // Cache for already loaded glyph maps
+    @GuardedBy("mGlyphMapLock")
+    private final SparseArray<KeyGlyphMap> mGlyphMapCache = new SparseArray<>();
+
+    KeyboardGlyphManager(Context context, Looper looper) {
+        mContext = context;
+        mHandler = new Handler(looper);
+    }
+
+    void systemRunning() {
+        if (!keyboardGlyphMap()) {
+            return;
+        }
+        // Listen to new Package installations to fetch new Keyboard glyph maps
+        IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
+        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+        filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
+        filter.addDataScheme("package");
+        mContext.registerReceiver(new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                resetMaps();
+            }
+        }, filter, null, mHandler);
+    }
+
+    @Override
+    @MainThread
+    public void onInputDeviceAdded(int deviceId) {
+    }
+
+    @Override
+    @MainThread
+    public void onInputDeviceRemoved(int deviceId) {
+        synchronized (mGlyphMapLock) {
+            mGlyphMapCache.remove(deviceId);
+        }
+    }
+
+    @Override
+    @MainThread
+    public void onInputDeviceChanged(int deviceId) {
+    }
+
+    @MainThread
+    private void resetMaps() {
+        synchronized (mGlyphMapLock) {
+            mGlyphMapDataLoaded = false;
+            mGlyphMapDataList.clear();
+            mGlyphMapCache.clear();
+        }
+    }
+
+    @NonNull
+    private List<KeyGlyphMapData> loadGlyphMapDataList() {
+        final PackageManager pm = mContext.getPackageManager();
+        List<KeyGlyphMapData> glyphMaps = new ArrayList<>();
+        Intent intent = new Intent(InputManager.ACTION_QUERY_KEYBOARD_GLYPH_MAPS);
+        for (ResolveInfo resolveInfo : pm.queryBroadcastReceiversAsUser(intent,
+                PackageManager.GET_META_DATA | PackageManager.MATCH_DIRECT_BOOT_AWARE
+                        | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, UserHandle.USER_SYSTEM)) {
+            if (resolveInfo == null || resolveInfo.activityInfo == null) {
+                continue;
+            }
+            final ActivityInfo activityInfo = resolveInfo.activityInfo;
+            KeyGlyphMapData data = getKeyboardGlyphMapsInPackage(pm, activityInfo);
+            if (data == null) {
+                continue;
+            }
+            glyphMaps.add(data);
+        }
+        return glyphMaps;
+    }
+
+    @Nullable
+    private KeyGlyphMapData getKeyboardGlyphMapsInPackage(PackageManager pm,
+            @NonNull ActivityInfo receiver) {
+        Bundle metaData = receiver.metaData;
+        if (metaData == null) {
+            return null;
+        }
+
+        int configResId = metaData.getInt(InputManager.META_DATA_KEYBOARD_GLYPH_MAPS);
+        if (configResId == 0) {
+            Slog.w(TAG, "Missing meta-data '" + InputManager.META_DATA_KEYBOARD_GLYPH_MAPS
+                    + "' on receiver " + receiver.packageName + "/" + receiver.name);
+            return null;
+        }
+
+        try {
+            Resources resources = pm.getResourcesForApplication(receiver.applicationInfo);
+            try (XmlResourceParser parser = resources.getXml(configResId)) {
+                XmlUtils.beginDocument(parser, TAG_KEYBOARD_GLYPH_MAPS);
+
+                while (true) {
+                    XmlUtils.nextElement(parser);
+                    String element = parser.getName();
+                    if (element == null) {
+                        break;
+                    }
+                    if (!TAG_KEYBOARD_GLYPH_MAP.equals(element)) {
+                        continue;
+                    }
+                    TypedArray a = resources.obtainAttributes(parser, R.styleable.KeyboardGlyphMap);
+                    try {
+                        int glyphMapRes = a.getResourceId(R.styleable.KeyboardGlyphMap_glyphMap, 0);
+                        int vendor = a.getInt(R.styleable.KeyboardGlyphMap_vendorId, -1);
+                        int product = a.getInt(R.styleable.KeyboardGlyphMap_productId, -1);
+                        if (glyphMapRes != 0 && vendor != -1 && product != -1) {
+                            return new KeyGlyphMapData(receiver.packageName, receiver.name,
+                                    glyphMapRes, vendor, product);
+                        }
+                    } finally {
+                        a.recycle();
+                    }
+                }
+            }
+        } catch (Exception ex) {
+            Slog.w(TAG, "Could not parse keyboard glyph map resource from receiver "
+                    + receiver.packageName + "/" + receiver.name, ex);
+        }
+        return null;
+    }
+
+    @Nullable
+    private KeyGlyphMap loadGlyphMap(KeyGlyphMapData data) {
+        final PackageManager pm = mContext.getPackageManager();
+        try {
+            ComponentName componentName = new ComponentName(data.packageName, data.receiverName);
+            ActivityInfo receiver = pm.getReceiverInfo(componentName,
+                    PackageManager.GET_META_DATA
+                            | PackageManager.MATCH_DIRECT_BOOT_AWARE
+                            | PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
+            Resources resources = pm.getResourcesForApplication(receiver.applicationInfo);
+            return loadGlyphMapFromResource(resources, componentName, data.resourceId);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.w(TAG, "Package not found: " + e);
+        }
+        return null;
+    }
+
+    @NonNull
+    private KeyGlyphMap loadGlyphMapFromResource(Resources resources,
+            @NonNull ComponentName componentName, @AnyRes int glyphMapId) {
+        SparseIntArray keyGlyphs = new SparseIntArray();
+        SparseIntArray modifierGlyphs = new SparseIntArray();
+        List<Integer> functionRowKeys = new ArrayList<>();
+        HashMap<KeyGlyphMap.KeyCombination, Integer> hardwareShortcuts = new HashMap<>();
+        try {
+            XmlResourceParser parser = resources.getXml(glyphMapId);
+            XmlUtils.beginDocument(parser, TAG_KEYBOARD_GLYPH_MAP);
+
+            while (true) {
+                XmlUtils.nextElement(parser);
+                String element = parser.getName();
+                if (element == null) {
+                    break;
+                }
+                switch (element) {
+                    case TAG_KEY_GLYPH -> {
+                        final TypedArray a = resources.obtainAttributes(parser,
+                                R.styleable.KeyGlyph);
+                        try {
+                            int keycode = a.getInt(R.styleable.KeyGlyph_keycode, 0);
+                            int keyGlyph = a.getResourceId(R.styleable.KeyGlyph_glyphDrawable, 0);
+                            if (keycode != 0 && keyGlyph != 0) {
+                                keyGlyphs.put(keycode, keyGlyph);
+                            }
+                        } finally {
+                            a.recycle();
+                        }
+                    }
+                    case TAG_MODIFIER_GLYPH -> {
+                        final TypedArray a = resources.obtainAttributes(parser,
+                                R.styleable.ModifierGlyph);
+                        try {
+                            int modifier = a.getInt(R.styleable.ModifierGlyph_modifier, 0);
+                            int modifierGlyph = a.getResourceId(
+                                    R.styleable.ModifierGlyph_glyphDrawable,
+                                    0);
+                            if (modifier != 0 && modifierGlyph != 0) {
+                                modifierGlyphs.put(modifier, modifierGlyph);
+                            }
+                        } finally {
+                            a.recycle();
+                        }
+                    }
+                    case TAG_FUNCTION_ROW_KEY -> {
+                        final TypedArray a = resources.obtainAttributes(parser,
+                                R.styleable.FunctionRowKey);
+                        try {
+                            int keycode = a.getInt(R.styleable.FunctionRowKey_keycode, 0);
+                            if (keycode != 0) {
+                                functionRowKeys.add(keycode);
+                            }
+                        } finally {
+                            a.recycle();
+                        }
+                    }
+                    case TAG_HARDWARE_DEFINED_SHORTCUT -> {
+                        final TypedArray a = resources.obtainAttributes(parser,
+                                R.styleable.HardwareDefinedShortcut);
+                        try {
+                            int keycode = a.getInt(R.styleable.HardwareDefinedShortcut_keycode,
+                                    0);
+                            int modifierState = a.getInt(
+                                    R.styleable.HardwareDefinedShortcut_modifierState, 0);
+                            int outKeycode = a.getInt(
+                                    R.styleable.HardwareDefinedShortcut_outKeycode,
+                                    0);
+                            if (keycode != 0 && modifierState != 0 && outKeycode != 0) {
+                                hardwareShortcuts.put(
+                                        new KeyGlyphMap.KeyCombination(modifierState, keycode),
+                                        outKeycode);
+                            }
+                        } finally {
+                            a.recycle();
+                        }
+                    }
+                }
+            }
+        } catch (XmlPullParserException | IOException e) {
+            Log.e(TAG, "Unable to parse key glyph map : " + e);
+        }
+        return new KeyGlyphMap(componentName, keyGlyphs, modifierGlyphs,
+                functionRowKeys.stream().mapToInt(Integer::intValue).toArray(), hardwareShortcuts);
+    }
+
+    /**
+     * Returns keyboard glyph map corresponding to device ID
+     */
+    @Nullable
+    public KeyGlyphMap getKeyGlyphMap(int deviceId) {
+        if (!keyboardGlyphMap()) {
+            return null;
+        }
+        synchronized (mGlyphMapLock) {
+            if (mGlyphMapCache.indexOfKey(deviceId) >= 0) {
+                return mGlyphMapCache.get(deviceId);
+            }
+            KeyGlyphMap keyGlyphMap = getKeyGlyphMapInternal(deviceId);
+            mGlyphMapCache.put(deviceId, keyGlyphMap);
+            return keyGlyphMap;
+        }
+    }
+
+    @GuardedBy("mGlyphMapLock")
+    private KeyGlyphMap getKeyGlyphMapInternal(int deviceId) {
+        final InputDevice inputDevice = getInputDevice(deviceId);
+        if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) {
+            return null;
+        }
+        if (!mGlyphMapDataLoaded) {
+            mGlyphMapDataList = loadGlyphMapDataList();
+            mGlyphMapDataLoaded = true;
+        }
+        for (KeyGlyphMapData data : mGlyphMapDataList) {
+            if (data.vendorId == inputDevice.getVendorId()
+                    && data.productId == inputDevice.getProductId()) {
+                return loadGlyphMap(data);
+            }
+        }
+        return null;
+    }
+
+    void dump(IndentingPrintWriter ipw) {
+        if (!keyboardGlyphMap()) {
+            return;
+        }
+        List<KeyGlyphMapData> glyphMapDataList = loadGlyphMapDataList();
+        ipw.println(TAG + ": " + glyphMapDataList.size() + " glyph maps");
+        ipw.increaseIndent();
+        for (KeyGlyphMapData data : glyphMapDataList) {
+            ipw.println(data);
+            if (DEBUG) {
+                KeyGlyphMap map = loadGlyphMap(data);
+                if (map != null) {
+                    ipw.increaseIndent();
+                    ipw.println(map);
+                    ipw.decreaseIndent();
+                }
+            }
+        }
+        ipw.decreaseIndent();
+    }
+
+    @Nullable
+    private InputDevice getInputDevice(int deviceId) {
+        InputManager inputManager = mContext.getSystemService(InputManager.class);
+        return inputManager != null ? inputManager.getInputDevice(deviceId) : null;
+    }
+
+    private record KeyGlyphMapData(@NonNull String packageName, @NonNull String receiverName,
+                                   @AnyRes int resourceId, int vendorId, int productId) {
+    }
+}
diff --git a/tests/Input/res/drawable/test_key_drawable.xml b/tests/Input/res/drawable/test_key_drawable.xml
new file mode 100644
index 0000000..2addf8f
--- /dev/null
+++ b/tests/Input/res/drawable/test_key_drawable.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <corners android:radius="4dp" />
+    <solid android:color="#ffffffff"/>
+</shape>
\ No newline at end of file
diff --git a/tests/Input/res/drawable/test_modifier_drawable.xml b/tests/Input/res/drawable/test_modifier_drawable.xml
new file mode 100644
index 0000000..2addf8f
--- /dev/null
+++ b/tests/Input/res/drawable/test_modifier_drawable.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <corners android:radius="4dp" />
+    <solid android:color="#ffffffff"/>
+</shape>
\ No newline at end of file
diff --git a/tests/Input/res/xml/keyboard_glyph_maps.xml b/tests/Input/res/xml/keyboard_glyph_maps.xml
new file mode 100644
index 0000000..d0616ff
--- /dev/null
+++ b/tests/Input/res/xml/keyboard_glyph_maps.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<keyboard-glyph-maps xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+    <keyboard-glyph-map
+        androidprv:glyphMap="@xml/test_glyph_map"
+        androidprv:vendorId="0x1234"
+        androidprv:productId="0x3456" />
+</keyboard-glyph-maps>
\ No newline at end of file
diff --git a/tests/Input/res/xml/test_glyph_map.xml b/tests/Input/res/xml/test_glyph_map.xml
new file mode 100644
index 0000000..7a7c1ac
--- /dev/null
+++ b/tests/Input/res/xml/test_glyph_map.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<keyboard-glyph-map xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+    <key-glyph
+        androidprv:keycode="KEYCODE_BACK"
+        androidprv:glyphDrawable="@drawable/test_key_drawable" />
+    <modifier-glyph
+        androidprv:modifier="META"
+        androidprv:glyphDrawable="@drawable/test_modifier_drawable" />
+    <function-row-key androidprv:keycode="KEYCODE_EMOJI_PICKER" />
+    <hardware-defined-shortcut
+        androidprv:keycode="KEYCODE_1"
+        androidprv:modifierState="FUNCTION"
+        androidprv:outKeycode="KEYCODE_BACK" />
+    <hardware-defined-shortcut
+        androidprv:keycode="KEYCODE_2"
+        androidprv:modifierState="FUNCTION|META"
+        androidprv:outKeycode="KEYCODE_HOME" />
+</keyboard-glyph-map>
\ No newline at end of file
diff --git a/tests/Input/src/com/android/server/input/KeyboardGlyphManagerTests.kt b/tests/Input/src/com/android/server/input/KeyboardGlyphManagerTests.kt
new file mode 100644
index 0000000..c073c7a
--- /dev/null
+++ b/tests/Input/src/com/android/server/input/KeyboardGlyphManagerTests.kt
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.input
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.content.pm.ServiceInfo
+import android.hardware.input.IInputManager
+import android.hardware.input.InputManager
+import android.hardware.input.InputManagerGlobal
+import android.hardware.input.KeyGlyphMap.KeyCombination
+import android.os.Bundle
+import android.os.test.TestLooper
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.annotations.Presubmit
+import android.platform.test.flag.junit.SetFlagsRule
+import android.view.InputDevice
+import android.view.KeyEvent
+import androidx.test.core.app.ApplicationProvider
+import com.android.hardware.input.Flags
+import com.android.test.input.R
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.junit.MockitoJUnit
+
+/**
+ * Tests for custom keyboard glyph map configuration.
+ *
+ * Build/Install/Run:
+ * atest InputTests:KeyboardGlyphManagerTests
+ */
+@Presubmit
+class KeyboardGlyphManagerTests {
+
+    companion object {
+        const val DEVICE_ID = 1
+        const val VENDOR_ID = 0x1234
+        const val PRODUCT_ID = 0x3456
+        const val PACKAGE_NAME = "KeyboardLayoutManagerTests"
+        const val RECEIVER_NAME = "DummyReceiver"
+    }
+
+    @JvmField
+    @Rule(order = 0)
+    val setFlagsRule = SetFlagsRule()
+
+    @JvmField
+    @Rule(order = 1)
+    val mockitoRule = MockitoJUnit.rule()!!
+
+    @Mock
+    private lateinit var packageManager: PackageManager
+
+    @Mock
+    private lateinit var iInputManager: IInputManager
+
+    private lateinit var keyboardGlyphManager: KeyboardGlyphManager
+    private lateinit var context: Context
+    private lateinit var testLooper: TestLooper
+    private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession
+    private lateinit var keyboardDevice: InputDevice
+
+    @Before
+    fun setup() {
+        context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext()))
+        inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManager)
+        testLooper = TestLooper()
+        keyboardGlyphManager = KeyboardGlyphManager(context, testLooper.looper)
+
+        setupInputDevices()
+        setupBroadcastReceiver()
+        keyboardGlyphManager.systemRunning()
+        testLooper.dispatchAll()
+    }
+
+    @After
+    fun tearDown() {
+        if (this::inputManagerGlobalSession.isInitialized) {
+            inputManagerGlobalSession.close()
+        }
+    }
+
+    private fun setupInputDevices() {
+        val inputManager = InputManager(context)
+        Mockito.`when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE)))
+            .thenReturn(inputManager)
+
+        keyboardDevice = createKeyboard(DEVICE_ID, VENDOR_ID, PRODUCT_ID, 0, "", "")
+        Mockito.`when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID))
+        Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardDevice)
+    }
+
+    private fun setupBroadcastReceiver() {
+        Mockito.`when`(context.packageManager).thenReturn(packageManager)
+
+        val info = createMockReceiver()
+        Mockito.`when`(packageManager.queryBroadcastReceiversAsUser(Mockito.any(), Mockito.anyInt(),
+            Mockito.anyInt())).thenReturn(listOf(info))
+        Mockito.`when`(packageManager.getReceiverInfo(Mockito.any(), Mockito.anyInt()))
+            .thenReturn(info.activityInfo)
+
+        val resources = context.resources
+        Mockito.`when`(
+            packageManager.getResourcesForApplication(
+                Mockito.any(
+                    ApplicationInfo::class.java
+                )
+            )
+        ).thenReturn(resources)
+    }
+
+    private fun createMockReceiver(): ResolveInfo {
+        val info = ResolveInfo()
+        info.activityInfo = ActivityInfo()
+        info.activityInfo.packageName = PACKAGE_NAME
+        info.activityInfo.name = RECEIVER_NAME
+        info.activityInfo.applicationInfo = ApplicationInfo()
+        info.activityInfo.metaData = Bundle()
+        info.activityInfo.metaData.putInt(
+            InputManager.META_DATA_KEYBOARD_GLYPH_MAPS,
+            R.xml.keyboard_glyph_maps
+        )
+        info.serviceInfo = ServiceInfo()
+        info.serviceInfo.packageName = PACKAGE_NAME
+        info.serviceInfo.name = RECEIVER_NAME
+        return info
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYBOARD_GLYPH_MAP)
+    fun testGlyphMapsLoaded() {
+        assertNotNull(
+            "Glyph map for test keyboard(deviceId=$DEVICE_ID) must exist",
+            keyboardGlyphManager.getKeyGlyphMap(DEVICE_ID)
+        )
+        assertNull(
+            "Glyph map for non-existing keyboard must be null",
+            keyboardGlyphManager.getKeyGlyphMap(-2)
+        )
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYBOARD_GLYPH_MAP)
+    fun testGlyphMapCorrectlyLoaded() {
+        val glyphMap = keyboardGlyphManager.getKeyGlyphMap(DEVICE_ID)
+        // Test glyph map used in this test: {@see test_glyph_map.xml}
+        assertNotNull(glyphMap!!.getDrawableForKeycode(context, KeyEvent.KEYCODE_BACK))
+
+        assertNotNull(glyphMap.getDrawableForModifier(context, KeyEvent.KEYCODE_META_LEFT))
+        assertNotNull(glyphMap.getDrawableForModifier(context, KeyEvent.KEYCODE_META_RIGHT))
+
+        val functionRowKeys = glyphMap.functionRowKeys
+        assertEquals(1, functionRowKeys.size)
+        assertEquals(KeyEvent.KEYCODE_EMOJI_PICKER, functionRowKeys[0])
+
+        val hardwareShortcuts = glyphMap.hardwareShortcuts
+        assertEquals(2, hardwareShortcuts.size)
+        assertEquals(
+            KeyEvent.KEYCODE_BACK,
+            hardwareShortcuts[KeyCombination(KeyEvent.META_FUNCTION_ON, KeyEvent.KEYCODE_1)]
+        )
+        assertEquals(
+            KeyEvent.KEYCODE_HOME,
+            hardwareShortcuts[
+                KeyCombination(
+                    KeyEvent.META_FUNCTION_ON or KeyEvent.META_META_ON,
+                    KeyEvent.KEYCODE_2
+                )
+            ]
+        )
+    }
+}
diff --git a/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt b/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt
index 93f97cb..301c0e6 100644
--- a/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt
@@ -58,7 +58,7 @@
 import java.io.IOException
 import java.io.InputStream
 
-private fun createKeyboard(
+fun createKeyboard(
     deviceId: Int,
     vendorId: Int,
     productId: Int,