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,