android_activity/input/
sdk.rs

1use std::sync::Arc;
2
3use jni::{
4    objects::{GlobalRef, JClass, JMethodID, JObject, JStaticMethodID, JValue},
5    signature::{Primitive, ReturnType},
6    JNIEnv,
7};
8use jni_sys::jint;
9
10use crate::{
11    input::{Keycode, MetaState},
12    jni_utils::CloneJavaVM,
13};
14
15use crate::{
16    error::{AppError, InternalAppError},
17    jni_utils,
18};
19
20/// An enum representing the types of keyboards that may generate key events
21///
22/// See [getKeyboardType() docs](https://developer.android.com/reference/android/view/KeyCharacterMap#getKeyboardType())
23///
24/// # Android Extensible Enum
25///
26/// This is a runtime [extensible enum](`crate#android-extensible-enums`) and
27/// should be handled similar to a `#[non_exhaustive]` enum to maintain
28/// forwards compatibility.
29///
30/// This implements `Into<u32>` and `From<u32>` for converting to/from Android
31/// SDK integer values.
32#[derive(
33    Debug, Clone, Copy, PartialEq, Eq, Hash, num_enum::FromPrimitive, num_enum::IntoPrimitive,
34)]
35#[non_exhaustive]
36#[repr(u32)]
37pub enum KeyboardType {
38    /// A numeric (12-key) keyboard.
39    ///
40    /// 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.
41    ///
42    /// This type of keyboard is generally designed for thumb typing.
43    Numeric,
44
45    /// A keyboard with all the letters, but with more than one letter per key.
46    ///
47    /// This type of keyboard is generally designed for thumb typing.
48    Predictive,
49
50    /// A keyboard with all the letters, and maybe some numbers.
51    ///
52    /// 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.
53    ///
54    /// This type of keyboard is generally designed for thumb typing.
55    Alpha,
56
57    /// A full PC-style keyboard.
58    ///
59    /// 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.
60    ///
61    /// This type of keyboard is generally designed for full two hand typing.
62    Full,
63
64    /// A keyboard that is only used to control special functions rather than for typing.
65    ///
66    /// A special function keyboard consists only of non-printing keys such as HOME and POWER that are not actually used for typing.
67    SpecialFunction,
68
69    #[doc(hidden)]
70    #[num_enum(catch_all)]
71    __Unknown(u32),
72}
73
74/// Either represents, a unicode character or combining accent from a
75/// [`KeyCharacterMap`], or `None` for non-printable keys.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
77pub enum KeyMapChar {
78    None,
79    Unicode(char),
80    CombiningAccent(char),
81}
82
83// I've also tried to think here about how to we could potentially automatically
84// generate a binding struct like `KeyCharacterMapBinding` with a procmacro and
85// so have intentionally limited the `Binding` being a very thin, un-opinionated
86// wrapper based on basic JNI types.
87
88/// Lower-level JNI binding for `KeyCharacterMap` class only holds 'static state
89/// and can be shared with an `Arc` ref count.
90///
91/// The separation here also neatly helps us separate `InternalAppError` from
92/// `AppError` for mapping JNI errors without exposing any `jni-rs` types in the
93/// public API.
94#[derive(Debug)]
95pub(crate) struct KeyCharacterMapBinding {
96    //vm: JavaVM,
97    klass: GlobalRef,
98    get_method_id: JMethodID,
99    get_dead_char_method_id: JStaticMethodID,
100    get_keyboard_type_method_id: JMethodID,
101}
102
103impl KeyCharacterMapBinding {
104    pub(crate) fn new(env: &mut JNIEnv) -> Result<Self, InternalAppError> {
105        let binding = env.with_local_frame::<_, _, InternalAppError>(10, |env| {
106            let klass = env.find_class("android/view/KeyCharacterMap")?; // Creates a local ref
107            Ok(Self {
108                get_method_id: env.get_method_id(&klass, "get", "(II)I")?,
109                get_dead_char_method_id: env.get_static_method_id(
110                    &klass,
111                    "getDeadChar",
112                    "(II)I",
113                )?,
114                get_keyboard_type_method_id: env.get_method_id(&klass, "getKeyboardType", "()I")?,
115                klass: env.new_global_ref(&klass)?,
116            })
117        })?;
118        Ok(binding)
119    }
120
121    pub fn get<'local>(
122        &self,
123        env: &'local mut JNIEnv,
124        key_map: impl AsRef<JObject<'local>>,
125        key_code: jint,
126        meta_state: jint,
127    ) -> Result<jint, InternalAppError> {
128        let key_map = key_map.as_ref();
129
130        // Safety:
131        // - we know our global `key_map` reference is non-null and valid.
132        // - we know `get_method_id` remains valid
133        // - we know that the signature of KeyCharacterMap::get is `(int, int) -> int`
134        // - we know this won't leak any local references as a side effect
135        //
136        // We know it's ok to unwrap the `.i()` value since we explicitly
137        // specify the return type as `Int`
138        let unicode = unsafe {
139            env.call_method_unchecked(
140                key_map,
141                self.get_method_id,
142                ReturnType::Primitive(Primitive::Int),
143                &[
144                    JValue::Int(key_code).as_jni(),
145                    JValue::Int(meta_state).as_jni(),
146                ],
147            )
148        }
149        .map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))?;
150        Ok(unicode.i().unwrap())
151    }
152
153    pub fn get_dead_char(
154        &self,
155        env: &mut JNIEnv,
156        accent_char: jint,
157        base_char: jint,
158    ) -> Result<jint, InternalAppError> {
159        // Safety:
160        // - we know `get_dead_char_method_id` remains valid
161        // - we know that KeyCharacterMap::getDeadKey is a static method
162        // - we know that the signature of KeyCharacterMap::getDeadKey is `(int, int) -> int`
163        // - we know this won't leak any local references as a side effect
164        //
165        // We know it's ok to unwrap the `.i()` value since we explicitly
166        // specify the return type as `Int`
167
168        // Urgh, it's pretty terrible that there's no ergonomic/safe way to get a JClass reference from a GlobalRef
169        // Safety: we don't do anything that would try to delete the JClass as if it were a real local reference
170        let klass = unsafe { JClass::from_raw(self.klass.as_obj().as_raw()) };
171        let unicode = unsafe {
172            env.call_static_method_unchecked(
173                &klass,
174                self.get_dead_char_method_id,
175                ReturnType::Primitive(Primitive::Int),
176                &[
177                    JValue::Int(accent_char).as_jni(),
178                    JValue::Int(base_char).as_jni(),
179                ],
180            )
181        }
182        .map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))?;
183        Ok(unicode.i().unwrap())
184    }
185
186    pub fn get_keyboard_type<'local>(
187        &self,
188        env: &'local mut JNIEnv,
189        key_map: impl AsRef<JObject<'local>>,
190    ) -> Result<jint, InternalAppError> {
191        let key_map = key_map.as_ref();
192
193        // Safety:
194        // - we know our global `key_map` reference is non-null and valid.
195        // - we know `get_keyboard_type_method_id` remains valid
196        // - we know that the signature of KeyCharacterMap::getKeyboardType is `() -> int`
197        // - we know this won't leak any local references as a side effect
198        //
199        // We know it's ok to unwrap the `.i()` value since we explicitly
200        // specify the return type as `Int`
201        Ok(unsafe {
202            env.call_method_unchecked(
203                key_map,
204                self.get_keyboard_type_method_id,
205                ReturnType::Primitive(Primitive::Int),
206                &[],
207            )
208        }
209        .map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))?
210        .i()
211        .unwrap())
212    }
213}
214
215/// Describes the keys provided by a keyboard device and their associated labels.
216#[derive(Clone, Debug)]
217pub struct KeyCharacterMap {
218    jvm: CloneJavaVM,
219    binding: Arc<KeyCharacterMapBinding>,
220    key_map: GlobalRef,
221}
222
223impl KeyCharacterMap {
224    pub(crate) fn new(
225        jvm: CloneJavaVM,
226        binding: Arc<KeyCharacterMapBinding>,
227        key_map: GlobalRef,
228    ) -> Self {
229        Self {
230            jvm,
231            binding,
232            key_map,
233        }
234    }
235
236    /// Gets the Unicode character generated by the specified [`Keycode`] and [`MetaState`] combination.
237    ///
238    /// Returns [`KeyMapChar::None`] if the key is not one that is used to type Unicode characters.
239    ///
240    /// Returns [`KeyMapChar::CombiningAccent`] if the key is a "dead key" that should be combined with
241    /// another to actually produce a character -- see [`KeyCharacterMap::get_dead_char`].
242    ///
243    /// # Errors
244    ///
245    /// Since this API needs to use JNI internally to call into the Android JVM it may return
246    /// a [`AppError::JavaError`] in case there is a spurious JNI error or an exception
247    /// is caught.
248    pub fn get(&self, key_code: Keycode, meta_state: MetaState) -> Result<KeyMapChar, AppError> {
249        let key_code: u32 = key_code.into();
250        let key_code = key_code as jni_sys::jint;
251        let meta_state: u32 = meta_state.0;
252        let meta_state = meta_state as jni_sys::jint;
253
254        // Since we expect this API to be called from the `main` thread then we expect to already be
255        // attached to the JVM
256        //
257        // Safety: there's no other JNIEnv in scope so this env can't be used to subvert the mutable
258        // borrow rules that ensure we can only add local references to the top JNI frame.
259        let mut env = self.jvm.get_env().map_err(|err| {
260            let err: InternalAppError = err.into();
261            err
262        })?;
263        let unicode = self
264            .binding
265            .get(&mut env, self.key_map.as_obj(), key_code, meta_state)?;
266        let unicode = unicode as u32;
267
268        const COMBINING_ACCENT: u32 = 0x80000000;
269        const COMBINING_ACCENT_MASK: u32 = !COMBINING_ACCENT;
270
271        if unicode == 0 {
272            Ok(KeyMapChar::None)
273        } else if unicode & COMBINING_ACCENT == COMBINING_ACCENT {
274            let accent = unicode & COMBINING_ACCENT_MASK;
275            // Safety: assumes Android key maps don't contain invalid unicode characters
276            Ok(KeyMapChar::CombiningAccent(unsafe {
277                char::from_u32_unchecked(accent)
278            }))
279        } else {
280            // Safety: assumes Android key maps don't contain invalid unicode characters
281            Ok(KeyMapChar::Unicode(unsafe {
282                char::from_u32_unchecked(unicode)
283            }))
284        }
285    }
286
287    /// Get the character that is produced by combining the dead key producing accent with the key producing character c.
288    ///
289    /// For example, ```get_dead_char('`', 'e')``` returns 'รจ'. `get_dead_char('^', ' ')` returns '^' and `get_dead_char('^', '^')` returns '^'.
290    ///
291    /// # Errors
292    ///
293    /// Since this API needs to use JNI internally to call into the Android JVM it may return
294    /// a [`AppError::JavaError`] in case there is a spurious JNI error or an exception
295    /// is caught.
296    pub fn get_dead_char(
297        &self,
298        accent_char: char,
299        base_char: char,
300    ) -> Result<Option<char>, AppError> {
301        let accent_char = accent_char as jni_sys::jint;
302        let base_char = base_char as jni_sys::jint;
303
304        // Since we expect this API to be called from the `main` thread then we expect to already be
305        // attached to the JVM
306        //
307        // Safety: there's no other JNIEnv in scope so this env can't be used to subvert the mutable
308        // borrow rules that ensure we can only add local references to the top JNI frame.
309        let mut env = self.jvm.get_env().map_err(|err| {
310            let err: InternalAppError = err.into();
311            err
312        })?;
313        let unicode = self
314            .binding
315            .get_dead_char(&mut env, accent_char, base_char)?;
316        let unicode = unicode as u32;
317
318        // Safety: assumes Android key maps don't contain invalid unicode characters
319        Ok(if unicode == 0 {
320            None
321        } else {
322            Some(unsafe { char::from_u32_unchecked(unicode) })
323        })
324    }
325
326    /// Gets the keyboard type.
327    ///
328    /// Different keyboard types have different semantics. See [`KeyboardType`] for details.
329    ///
330    /// # Errors
331    ///
332    /// Since this API needs to use JNI internally to call into the Android JVM it may return
333    /// a [`AppError::JavaError`] in case there is a spurious JNI error or an exception
334    /// is caught.
335    pub fn get_keyboard_type(&self) -> Result<KeyboardType, AppError> {
336        // Since we expect this API to be called from the `main` thread then we expect to already be
337        // attached to the JVM
338        //
339        // Safety: there's no other JNIEnv in scope so this env can't be used to subvert the mutable
340        // borrow rules that ensure we can only add local references to the top JNI frame.
341        let mut env = self.jvm.get_env().map_err(|err| {
342            let err: InternalAppError = err.into();
343            err
344        })?;
345        let keyboard_type = self
346            .binding
347            .get_keyboard_type(&mut env, self.key_map.as_obj())?;
348        let keyboard_type = keyboard_type as u32;
349        Ok(keyboard_type.into())
350    }
351}