Skip to main content

android_activity/input/
sdk.rs

1use jni::sys::jint;
2use jni::{objects::Global, JavaVM};
3
4use crate::error::{AppError, InternalAppError, InternalResult};
5use crate::input::{Keycode, MetaState};
6use crate::jni_utils;
7
8/// An enum representing the types of keyboards that may generate key events
9///
10/// See [getKeyboardType() docs](https://developer.android.com/reference/android/view/KeyCharacterMap#getKeyboardType())
11///
12/// # Android Extensible Enum
13///
14/// This is a runtime [extensible enum](`crate#android-extensible-enums`) and
15/// should be handled similar to a `#[non_exhaustive]` enum to maintain
16/// forwards compatibility.
17///
18/// This implements `Into<u32>` and `From<u32>` for converting to/from Android
19/// SDK integer values.
20#[derive(
21    Debug, Clone, Copy, PartialEq, Eq, Hash, num_enum::FromPrimitive, num_enum::IntoPrimitive,
22)]
23#[non_exhaustive]
24#[repr(u32)]
25pub enum KeyboardType {
26    /// A numeric (12-key) keyboard.
27    ///
28    /// A numeric keyboard supports text entry using a multi-tap approach. It may be necessary to tap a key multiple times to generate the desired letter or symbol.
29    ///
30    /// This type of keyboard is generally designed for thumb typing.
31    Numeric,
32
33    /// A keyboard with all the letters, but with more than one letter per key.
34    ///
35    /// This type of keyboard is generally designed for thumb typing.
36    Predictive,
37
38    /// A keyboard with all the letters, and maybe some numbers.
39    ///
40    /// An alphabetic keyboard supports text entry directly but may have a condensed layout with a small form factor. In contrast to a full keyboard, some symbols may only be accessible using special on-screen character pickers. In addition, to improve typing speed and accuracy, the framework provides special affordances for alphabetic keyboards such as auto-capitalization and toggled / locked shift and alt keys.
41    ///
42    /// This type of keyboard is generally designed for thumb typing.
43    Alpha,
44
45    /// A full PC-style keyboard.
46    ///
47    /// A full keyboard behaves like a PC keyboard. All symbols are accessed directly by pressing keys on the keyboard without on-screen support or affordances such as auto-capitalization.
48    ///
49    /// This type of keyboard is generally designed for full two hand typing.
50    Full,
51
52    /// A keyboard that is only used to control special functions rather than for typing.
53    ///
54    /// A special function keyboard consists only of non-printing keys such as HOME and POWER that are not actually used for typing.
55    SpecialFunction,
56
57    #[doc(hidden)]
58    #[num_enum(catch_all)]
59    __Unknown(u32),
60}
61
62/// Either represents, a unicode character or combining accent from a
63/// [`KeyCharacterMap`], or `None` for non-printable keys.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65pub enum KeyMapChar {
66    None,
67    Unicode(char),
68    CombiningAccent(char),
69}
70
71jni::bind_java_type! {
72    pub(crate) AKeyCharacterMap => "android.view.KeyCharacterMap",
73    methods {
74        priv fn _get(key_code: jint, meta_state: jint) -> jint,
75        priv static fn _get_dead_char(accent_char: jint, base_char: jint) -> jint,
76        priv fn _get_keyboard_type() -> jint,
77    }
78}
79
80impl AKeyCharacterMap<'_> {
81    pub(crate) fn get(
82        &self,
83        env: &mut jni::Env,
84        key_code: jint,
85        meta_state: jint,
86    ) -> Result<jint, InternalAppError> {
87        self._get(env, key_code, meta_state)
88            .map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))
89    }
90
91    pub(crate) fn get_dead_char(
92        env: &mut jni::Env,
93        accent_char: jint,
94        base_char: jint,
95    ) -> Result<jint, InternalAppError> {
96        Self::_get_dead_char(env, accent_char, base_char)
97            .map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))
98    }
99
100    pub(crate) fn get_keyboard_type(&self, env: &mut jni::Env) -> Result<jint, InternalAppError> {
101        self._get_keyboard_type(env)
102            .map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))
103    }
104}
105
106jni::bind_java_type! {
107    pub(crate) AInputDevice => "android.view.InputDevice",
108    type_map {
109        AKeyCharacterMap => "android.view.KeyCharacterMap",
110    },
111    methods {
112        static fn get_device(id: jint) -> AInputDevice,
113        fn get_key_character_map() -> AKeyCharacterMap,
114    }
115}
116
117/// Describes the keys provided by a keyboard device and their associated labels.
118#[derive(Debug)]
119pub struct KeyCharacterMap {
120    jvm: JavaVM,
121    key_map: Global<AKeyCharacterMap<'static>>,
122}
123impl Clone for KeyCharacterMap {
124    fn clone(&self) -> Self {
125        let jvm = self.jvm.clone();
126        jvm.attach_current_thread(|env| -> jni::errors::Result<_> {
127            Ok(Self {
128                jvm: jvm.clone(),
129                key_map: env.new_global_ref(&self.key_map)?,
130            })
131        })
132        .expect("Failed to attach thread to JVM and clone key map")
133    }
134}
135
136impl KeyCharacterMap {
137    pub(crate) fn new(jvm: JavaVM, key_map: Global<AKeyCharacterMap<'static>>) -> Self {
138        Self { jvm, key_map }
139    }
140
141    /// Gets the Unicode character generated by the specified [`Keycode`] and [`MetaState`] combination.
142    ///
143    /// Returns [`KeyMapChar::None`] if the key is not one that is used to type Unicode characters.
144    ///
145    /// Returns [`KeyMapChar::CombiningAccent`] if the key is a "dead key" that should be combined with
146    /// another to actually produce a character -- see [`KeyCharacterMap::get_dead_char`].
147    ///
148    /// # Errors
149    ///
150    /// Since this API needs to use JNI internally to call into the Android JVM it may return
151    /// a [`AppError::JavaError`] in case there is a spurious JNI error or an exception
152    /// is caught.
153    pub fn get(&self, key_code: Keycode, meta_state: MetaState) -> Result<KeyMapChar, AppError> {
154        let key_code: u32 = key_code.into();
155        let key_code = key_code as jni::sys::jint;
156        let meta_state: u32 = meta_state.0;
157        let meta_state = meta_state as jni::sys::jint;
158
159        let vm = self.jvm.clone();
160        vm.attach_current_thread(|env| -> InternalResult<_> {
161            let unicode = self.key_map.get(env, key_code, meta_state)?;
162            let unicode = unicode as u32;
163
164            const COMBINING_ACCENT: u32 = 0x80000000;
165            const COMBINING_ACCENT_MASK: u32 = !COMBINING_ACCENT;
166
167            if unicode == 0 {
168                Ok(KeyMapChar::None)
169            } else if unicode & COMBINING_ACCENT == COMBINING_ACCENT {
170                let accent = unicode & COMBINING_ACCENT_MASK;
171                // Safety: assumes Android key maps don't contain invalid unicode characters
172                Ok(KeyMapChar::CombiningAccent(unsafe {
173                    char::from_u32_unchecked(accent)
174                }))
175            } else {
176                // Safety: assumes Android key maps don't contain invalid unicode characters
177                Ok(KeyMapChar::Unicode(unsafe {
178                    char::from_u32_unchecked(unicode)
179                }))
180            }
181        })
182        .map_err(|err| {
183            let err: InternalAppError = err;
184            err.into()
185        })
186    }
187
188    /// Get the character that is produced by combining the dead key producing accent with the key producing character c.
189    ///
190    /// For example, ``get_dead_char('`', 'e')`` returns `'รจ'`. `get_dead_char('^', ' ')` returns `'^'` and `get_dead_char('^', '^')` returns `'^'`.
191    ///
192    /// # Errors
193    ///
194    /// Since this API needs to use JNI internally to call into the Android JVM it may return a
195    /// [`AppError::JavaError`] in case there is a spurious JNI error or an exception is caught.
196    pub fn get_dead_char(
197        &self,
198        accent_char: char,
199        base_char: char,
200    ) -> Result<Option<char>, AppError> {
201        let accent_char = accent_char as jni::sys::jint;
202        let base_char = base_char as jni::sys::jint;
203
204        let vm = self.jvm.clone();
205        vm.attach_current_thread(|env| -> InternalResult<_> {
206            let unicode = AKeyCharacterMap::get_dead_char(env, accent_char, base_char)?;
207            let unicode = unicode as u32;
208
209            // Safety: assumes Android key maps don't contain invalid unicode characters
210            Ok(if unicode == 0 {
211                None
212            } else {
213                Some(unsafe { char::from_u32_unchecked(unicode) })
214            })
215        })
216        .map_err(|err| {
217            let err: InternalAppError = err;
218            err.into()
219        })
220    }
221
222    /// Gets the keyboard type.
223    ///
224    /// Different keyboard types have different semantics. See [`KeyboardType`] for details.
225    ///
226    /// # Errors
227    ///
228    /// Since this API needs to use JNI internally to call into the Android JVM it may return
229    /// a [`AppError::JavaError`] in case there is a spurious JNI error or an exception
230    /// is caught.
231    pub fn get_keyboard_type(&self) -> Result<KeyboardType, AppError> {
232        let vm = self.jvm.clone();
233        vm.attach_current_thread(|env| -> InternalResult<_> {
234            let keyboard_type = self.key_map.get_keyboard_type(env)?;
235            let keyboard_type = keyboard_type as u32;
236            Ok(keyboard_type.into())
237        })
238        .map_err(|err| {
239            let err: InternalAppError = err;
240            err.into()
241        })
242    }
243}
244
245fn device_key_character_map_with_env(
246    env: &mut jni::Env<'_>,
247    device_id: i32,
248) -> jni::errors::Result<KeyCharacterMap> {
249    let device = AInputDevice::get_device(env, device_id)?;
250    if device.is_null() {
251        // This isn't really an error from a JNI perspective but we would only expect
252        // this to return null for a device ID of zero or an invalid device ID.
253        log::error!("No input device with id {}", device_id);
254        return Err(jni::errors::Error::WrongObjectType);
255    }
256    let character_map = device.get_key_character_map(env)?;
257    let character_map = env.new_global_ref(character_map)?;
258    let jvm = JavaVM::singleton().expect("Failed to get singleton JavaVM");
259    Ok(KeyCharacterMap::new(jvm, character_map))
260}
261
262pub(crate) fn device_key_character_map(
263    jvm: JavaVM,
264    device_id: i32,
265) -> InternalResult<KeyCharacterMap> {
266    jvm.attach_current_thread(|env| {
267        if device_id == 0 {
268            return Err(InternalAppError::JniBadArgument(
269                "Can't get key character map for non-physical device_id 0".into(),
270            ));
271        }
272        device_key_character_map_with_env(env, device_id)
273            .map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))
274    })
275}