From c81ffe5959fc496f7743357641a57a70fd2411e4 Mon Sep 17 00:00:00 2001 From: lorenzomicheli Date: Fri, 16 Oct 2026 13:23:40 +0000 Subject: [PATCH] android: Clear and describe Java exceptions in the core JNI calls A call into Java that throws leaves the exception pending. jni 0.22 then refuses every later JNI call in the same frame, and the error only says "JavaException". Several core calls converted the error with map_err(to_string) and left the exception pending; others cleared it by hand, so its message was lost, or dropped the error with `let _ =`. The new JniResultExt::or_clear clears the pending exception and returns its toString(), so errors read like "java.lang.SecurityException: ...". or_catch makes an expected exception explicit: the system chrome now falls back to the pre-API-30 flags only on NoSuchMethodError, where any failure of getInsetsController used to. The calls into Java in jni.rs, host.rs, platform.rs, window.rs, platform_view.rs and document_picker.rs use them, and their errors are returned or logged. get_string now checks that the object is a java.lang.String before reading it; before, any object handed to it was read as one. The platform packages are left for a follow-up. Part of #31. Co-Authored-By: Claude Opus 5.5 --- src/android/document_picker.rs | 7 +- src/android/host.rs | 16 +- src/android/jni.rs | 361 ++++++++++++++++++++------------- src/android/platform.rs | 37 ++-- src/android/platform_view.rs | 124 ++++++----- src/android/window.rs | 47 ++--- 6 files changed, 313 insertions(+), 279 deletions(-) diff --git a/src/android/document_picker.rs b/src/android/document_picker.rs index e460c80a..fb99a46a 100644 --- a/src/android/document_picker.rs +++ b/src/android/document_picker.rs @@ -1,6 +1,6 @@ //! Android Storage Access Framework bridge returning readable local files. -use super::jni::{self as jni_helpers, JniExt}; +use super::jni::{self as jni_helpers, JniExt, JniResultExt as _}; use anyhow::{anyhow, Result}; use futures::channel::oneshot; use gpui::PathPromptOptions; @@ -38,10 +38,7 @@ fn pick_files(multiple: bool) -> std::result::Result>, Strin &[JValue::Object(&activity), JValue::Bool(multiple)], ) .and_then(|value| value.l()) - .map_err(|error| { - env.exception_clear(); - error.to_string() - })?; + .or_clear(env)?; if result.is_null() { return Ok(None); } diff --git a/src/android/host.rs b/src/android/host.rs index d595adb8..411ee18d 100644 --- a/src/android/host.rs +++ b/src/android/host.rs @@ -76,6 +76,7 @@ use gpui::{App, Application, ApplicationHandle}; use jni::{objects::JObject, refs::Global}; use ndk::native_window::NativeWindow; +use super::jni::JniResultExt as _; use super::platform::{AndroidPlatform, SharedPlatform}; use crate::android::{AndroidKeyEvent, TouchPoint}; @@ -577,18 +578,9 @@ pub fn set_activity( if super::jni::android_app().is_some() { return Ok(()); } - match env.new_global_ref(activity) { - Ok(activity) => { - ACTIVITIES.lock().expect("poisoned").insert(host, activity); - Ok(()) - } - Err(err) => { - if env.exception_check() { - env.exception_clear(); - } - Err(err.to_string()) - } - } + let activity = env.new_global_ref(activity).or_clear(env)?; + ACTIVITIES.lock().expect("poisoned").insert(host, activity); + Ok(()) } /// Run `f` with `host`'s own Activity (a global `jobject`), or with the newest one if diff --git a/src/android/jni.rs b/src/android/jni.rs index cfd56944..1eac4ae5 100644 --- a/src/android/jni.rs +++ b/src/android/jni.rs @@ -91,7 +91,7 @@ use android_activity::{AndroidApp, MainEvent, PollEvent}; use super::platform::{AndroidPlatform, SharedPlatform}; -use jni::objects::{JObject, JString, JValue}; +use jni::objects::{JObject, JString, JThrowable, JValue}; use jni::JavaVM; use std::sync::{atomic::AtomicPtr, Mutex}; @@ -152,16 +152,34 @@ pub fn activity<'local>(env: &jni::Env<'local>) -> Result, Strin /// Convert a Java String (`JObject` wrapping a `java.lang.String`) to a Rust `String`. /// -/// Returns an empty string on null or error. +/// Returns an empty string on null, on an object that is not a `String`, or on error. pub fn get_string(env: &mut jni::Env<'_>, obj: &JObject<'_>) -> String { if obj.is_null() { return String::new(); } + match env.is_instance_of(obj, jni::jni_str!("java/lang/String")) { + Ok(true) => {} + Ok(false) => { + log::warn!("get_string: not a java.lang.String"); + return String::new(); + } + Err(err) => { + log::warn!( + "get_string: {}", + take_exception(env).unwrap_or_else(|| err.to_string()) + ); + return String::new(); + } + } + // SAFETY: `obj` is a live, non-null reference to a `java.lang.String`. let jstr = unsafe { JString::from_raw(env, obj.as_raw()) }; jstr.to_string() } /// Extension trait for converting `jni::errors::Result` to `Result`. +/// +/// Leaves a Java exception pending; use [`JniResultExt::or_clear`] for calls into +/// Java. pub(crate) trait JniExt { fn e(self) -> Result; } @@ -172,6 +190,74 @@ impl JniExt for jni::errors::Result { } } +/// Error handling for calls into Java. +pub(crate) trait JniResultExt { + /// On error, clear the pending Java exception, if any, and describe it: later + /// JNI calls in the same frame would otherwise fail too, and the bare JNI error + /// only says that an exception was thrown. + fn or_clear(self, env: &mut jni::Env<'_>) -> Result; + + /// Like [`or_clear`](Self::or_clear), but an exception that is an instance of + /// `class` (a JNI class name, e.g. `java/lang/NoSuchMethodError`) is an expected + /// outcome: it is cleared and the result is `Ok(None)`. + fn or_catch( + self, + env: &mut jni::Env<'_>, + class: &'static jni::strings::JNIStr, + ) -> Result, String>; +} + +impl JniResultExt for jni::errors::Result { + fn or_clear(self, env: &mut jni::Env<'_>) -> Result { + self.map_err(|err| take_exception(env).unwrap_or_else(|| err.to_string())) + } + + fn or_catch( + self, + env: &mut jni::Env<'_>, + class: &'static jni::strings::JNIStr, + ) -> Result, String> { + let err = match self { + Ok(value) => return Ok(Some(value)), + Err(err) => err, + }; + let Some(throwable) = env.exception_occurred() else { + return Err(err.to_string()); + }; + env.exception_clear(); + if env.is_instance_of(&throwable, class).unwrap_or(false) { + return Ok(None); + } + Err(describe_throwable(env, &throwable)) + } +} + +/// Clear the pending Java exception and return its `toString()`, if there is one. +pub(crate) fn take_exception(env: &mut jni::Env<'_>) -> Option { + let throwable = env.exception_occurred()?; + env.exception_clear(); + Some(describe_throwable(env, &throwable)) +} + +/// `throwable.toString()`, e.g. `java.lang.SecurityException: `. +fn describe_throwable(env: &mut jni::Env<'_>, throwable: &JThrowable<'_>) -> String { + let description = env + .call_method( + throwable, + jni::jni_str!("toString"), + jni::jni_sig!("()Ljava/lang/String;"), + &[], + ) + .and_then(|value| value.l()); + match description { + Ok(description) => get_string(env, &description), + Err(_) => { + env.exception_clear(); + "a Java exception was thrown".into() + } + } +} + /// Find an application class by name using the Activity's classloader. /// /// From native threads, `JNIEnv::FindClass` uses the system classloader @@ -196,15 +282,15 @@ pub fn find_app_class<'local>( &[], ) .and_then(|v| v.l()) + .or_clear(env) .map_err(|e| { - env.exception_clear(); let msg = format!("getClassLoader failed: {e}"); log::error!("find_app_class({class_name}): {msg}"); msg })?; // classLoader.loadClass("dev.gpui.mobile.GpuiHelper") - let jname = env.new_string(class_name).e()?; + let jname = env.new_string(class_name).or_clear(env)?; let loaded = env .call_method( &class_loader, @@ -213,10 +299,8 @@ pub fn find_app_class<'local>( &[JValue::Object(&jname)], ) .and_then(|v| v.l()) + .or_clear(env) .map_err(|e| { - // Print full Java stack trace to logcat, then clear. - env.exception_describe(); - env.exception_clear(); let msg = format!("loadClass({class_name}) failed: {e}"); log::error!("{msg}"); msg @@ -244,34 +328,28 @@ static PLATFORM: OnceLock> = OnceLock::new(); /// `getUnicodeChar(metaState)` on it. Returns 0 on failure. pub fn unicode_char_for_key_event(key_code: i32, action: i32, meta_state: i32) -> u32 { with_env(|env| { - let key_event = match env.new_object( - jni::jni_str!("android/view/KeyEvent"), - jni::jni_sig!("(II)V"), - &[JValue::Int(action), JValue::Int(key_code)], - ) { - Ok(o) => o, - Err(_) => { - env.exception_clear(); - return Ok(0); - } - }; - match env.call_method( - &key_event, - jni::jni_str!("getUnicodeChar"), - jni::jni_sig!("(I)I"), - &[JValue::Int(meta_state)], - ) { - Ok(v) => { - let c = v.i().unwrap_or(0); - Ok(if c > 0 { c as u32 } else { 0 }) - } - Err(_) => { - env.exception_clear(); - Ok(0) - } - } + let key_event = env + .new_object( + jni::jni_str!("android/view/KeyEvent"), + jni::jni_sig!("(II)V"), + &[JValue::Int(action), JValue::Int(key_code)], + ) + .or_clear(env)?; + let c = env + .call_method( + &key_event, + jni::jni_str!("getUnicodeChar"), + jni::jni_sig!("(I)I"), + &[JValue::Int(meta_state)], + ) + .and_then(|v| v.i()) + .or_clear(env)?; + Ok(c.max(0) as u32) + }) + .unwrap_or_else(|err| { + log::warn!("unicode_char_for_key_event: {err}"); + 0 }) - .unwrap_or(0) } // ── public accessors ────────────────────────────────────────────────────────── @@ -311,13 +389,9 @@ struct HostActivity { /// Call it before `host::start`: the platform reads bundled assets (the emoji font) /// when it starts. If `start` came first, the emoji font is loaded here instead. pub fn set_host_activity(env: &mut jni::Env<'_>, activity: &JObject<'_>) -> Result<(), String> { - let result = register_host_activity(env, activity); // The caller may make more JNI calls before returning to Java, which a pending // exception would make undefined. - if result.is_err() && env.exception_check() { - env.exception_clear(); - } - result + register_host_activity(env, activity).map_err(|err| take_exception(env).unwrap_or(err)) } fn register_host_activity(env: &mut jni::Env<'_>, activity: &JObject<'_>) -> Result<(), String> { @@ -343,7 +417,7 @@ fn register_host_activity(env: &mut jni::Env<'_>, activity: &JObject<'_>) -> Res .l()?; env.new_global_ref(&context) }) - .e()?; + .or_clear(env)?; let _ = HOST_CONTEXT.set(context); } let mut slot = HOST_ACTIVITY.lock().expect("poisoned"); @@ -356,12 +430,7 @@ fn register_host_activity(env: &mut jni::Env<'_>, activity: &JObject<'_>) -> Res let _ = HOST_ASSETS.set(assets); super::platform::load_deferred_emoji_font(); } - Err(err) => { - if env.exception_check() { - env.exception_clear(); - } - log::warn!("set_host_activity: no AssetManager: {err}"); - } + Err(err) => log::warn!("set_host_activity: no AssetManager: {err}"), } } Ok(()) @@ -384,7 +453,7 @@ fn host_assets(env: &mut jni::Env<'_>, activity: &JObject<'_>) -> Result(host_assets_in_frame(env, activity)) }) - .e()? + .or_clear(env)? } fn host_assets_in_frame( @@ -399,7 +468,7 @@ fn host_assets_in_frame( &[], ) .and_then(|value| value.l()) - .e()?; + .or_clear(env)?; let assets = env .call_method( &context, @@ -408,7 +477,7 @@ fn host_assets_in_frame( &[], ) .and_then(|value| value.l()) - .e()?; + .or_clear(env)?; if assets.is_null() { return Err("getAssets returned null".into()); } @@ -577,10 +646,7 @@ fn host_ui_mode() -> Result { env.get_field(&config, jni::jni_str!("uiMode"), jni::jni_sig!("I"))? .i() }); - if result.is_err() { - env.exception_clear(); - } - result.e() + result.or_clear(env) }) } @@ -1076,13 +1142,15 @@ pub fn run_event_loop(app: &AndroidApp) { fn pause_platform_views() { let _ = with_env(|env| { if let Ok(helper_class) = find_app_class(env, "dev.gpui.mobile.GpuiPlatformView") { - let _ = env.call_static_method( + let result = env.call_static_method( &helper_class, jni::jni_str!("pauseAll"), jni::jni_sig!("()V"), &[], ); - env.exception_clear(); + if let Err(err) = result.or_clear(env) { + log::warn!("pauseAll: {err}"); + } } Ok(()) }); @@ -1092,13 +1160,15 @@ fn pause_platform_views() { fn resume_platform_views() { let _ = with_env(|env| { if let Ok(helper_class) = find_app_class(env, "dev.gpui.mobile.GpuiPlatformView") { - let _ = env.call_static_method( + let result = env.call_static_method( &helper_class, jni::jni_str!("resumeAll"), jni::jni_sig!("()V"), &[], ); - env.exception_clear(); + if let Err(err) = result.or_clear(env) { + log::warn!("resumeAll: {err}"); + } } Ok(()) }); @@ -1108,13 +1178,15 @@ fn resume_platform_views() { fn dispose_all_platform_views() { let _ = with_env(|env| { if let Ok(helper_class) = find_app_class(env, "dev.gpui.mobile.GpuiPlatformView") { - let _ = env.call_static_method( + let result = env.call_static_method( &helper_class, jni::jni_str!("disposeAll"), jni::jni_sig!("()V"), &[], ); - env.exception_clear(); + if let Err(err) = result.or_clear(env) { + log::warn!("disposeAll: {err}"); + } } Ok(()) }); @@ -1356,10 +1428,7 @@ pub fn set_system_chrome(style: &crate::SystemChromeStyle) { &[], ) .and_then(|v: jni::objects::JValueOwned| v.l()) - .map_err(|e| { - env.exception_clear(); - e.to_string() - })?; + .or_clear(env)?; if window.is_null() { return Err("getWindow returned null".into()); } @@ -1367,87 +1436,97 @@ pub fn set_system_chrome(style: &crate::SystemChromeStyle) { // 2. Set status bar color if provided if let Some(color) = status_bar_color { let argb = (0xFF000000_u32 | color) as i32; - let _ = env.call_method( + let result = env.call_method( &window, jni::jni_str!("setStatusBarColor"), jni::jni_sig!("(I)V"), &[JValue::Int(argb)], ); - env.exception_clear(); + if let Err(err) = result.or_clear(env) { + log::warn!("setStatusBarColor: {err}"); + } } // 3. Set navigation bar color if provided if let Some(color) = navigation_bar_color { let argb = (0xFF000000_u32 | color) as i32; - let _ = env.call_method( + let result = env.call_method( &window, jni::jni_str!("setNavigationBarColor"), jni::jni_sig!("(I)V"), &[JValue::Int(argb)], ); - env.exception_clear(); - } - - // 4. Set light/dark status bar icons via WindowInsetsController (API 30+) - let insetsctl = env.call_method( - &window, - jni::jni_str!("getInsetsController"), - jni::jni_sig!("()Landroid/view/WindowInsetsController;"), - &[], - ); - - if let Ok(v) = insetsctl { - if let Ok(ctl) = v.l() { - if !ctl.is_null() { - let mask: i32 = 0x00000008; - let appearance: i32 = match status_bar_style { - crate::StatusBarContentStyle::Dark => 0x00000008, - crate::StatusBarContentStyle::Light => 0, - }; - let _ = env.call_method( - &ctl, - jni::jni_str!("setSystemBarsAppearance"), - jni::jni_sig!("(II)V"), - &[JValue::Int(appearance), JValue::Int(mask)], - ); - env.exception_clear(); - } + if let Err(err) = result.or_clear(env) { + log::warn!("setNavigationBarColor: {err}"); } - } else { - env.exception_clear(); + } - if let Ok(decor) = env - .call_method( - &window, - jni::jni_str!("getDecorView"), - jni::jni_sig!("()Landroid/view/View;"), - &[], + // 4. Set light/dark status bar icons via WindowInsetsController (API 30+), + // or the deprecated system UI visibility flags before it. + let controller = env + .call_method( + &window, + jni::jni_str!("getInsetsController"), + jni::jni_sig!("()Landroid/view/WindowInsetsController;"), + &[], + ) + .and_then(|v| v.l()) + .or_catch(env, jni::jni_str!("java/lang/NoSuchMethodError"))?; + match controller { + Some(controller) if !controller.is_null() => { + const APPEARANCE_LIGHT_STATUS_BARS: i32 = 0x0000_0008; + let appearance = match status_bar_style { + crate::StatusBarContentStyle::Dark => APPEARANCE_LIGHT_STATUS_BARS, + crate::StatusBarContentStyle::Light => 0, + }; + env.call_method( + &controller, + jni::jni_str!("setSystemBarsAppearance"), + jni::jni_sig!("(II)V"), + &[ + JValue::Int(appearance), + JValue::Int(APPEARANCE_LIGHT_STATUS_BARS), + ], ) - .and_then(|v: jni::objects::JValueOwned| v.l()) - { - if !decor.is_null() { - if let Ok(current) = env - .call_method( - &decor, - jni::jni_str!("getSystemUiVisibility"), - jni::jni_sig!("()I"), - &[], - ) - .and_then(|v: jni::objects::JValueOwned| v.i()) - { - let new_flags = match status_bar_style { - crate::StatusBarContentStyle::Dark => current | 0x00002000, - crate::StatusBarContentStyle::Light => current & !0x00002000, - }; - let _ = env.call_method( - &decor, - jni::jni_str!("setSystemUiVisibility"), - jni::jni_sig!("(I)V"), - &[JValue::Int(new_flags)], - ); - env.exception_clear(); - } + .or_clear(env)?; + } + Some(_) => {} + None => { + const SYSTEM_UI_FLAG_LIGHT_STATUS_BAR: i32 = 0x0000_2000; + let decor = env + .call_method( + &window, + jni::jni_str!("getDecorView"), + jni::jni_sig!("()Landroid/view/View;"), + &[], + ) + .and_then(|v| v.l()) + .or_clear(env)?; + if decor.is_null() { + return Err("getDecorView returned null".into()); } + let current = env + .call_method( + &decor, + jni::jni_str!("getSystemUiVisibility"), + jni::jni_sig!("()I"), + &[], + ) + .and_then(|v| v.i()) + .or_clear(env)?; + let flags = match status_bar_style { + crate::StatusBarContentStyle::Dark => current | SYSTEM_UI_FLAG_LIGHT_STATUS_BAR, + crate::StatusBarContentStyle::Light => { + current & !SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + } + }; + env.call_method( + &decor, + jni::jni_str!("setSystemUiVisibility"), + jni::jni_sig!("(I)V"), + &[JValue::Int(flags)], + ) + .or_clear(env)?; } } @@ -1541,14 +1620,10 @@ pub fn show_keyboard_android(keyboard_type: crate::KeyboardType) { jni::jni_sig!("(IJ)V"), &[JValue::Int(kind), JValue::Long(session as i64)], ) - .map_err(|e| e.to_string())?; + .or_clear(env)?; Ok(()) }) { log::warn!("IME requires GpuiInputActivity: {error}"); - let _ = with_env(|env| { - env.exception_clear(); - Ok(()) - }); } } @@ -1559,18 +1634,19 @@ pub fn hide_keyboard_android() { *SHOWN_KEYBOARD.lock().expect("poisoned") = None; *DISMISSED_KEYBOARD.lock().expect("poisoned") = None; let session = super::text_input::new_session(); - let _ = with_env(|env| { + if let Err(err) = with_env(|env| { let activity = activity(env)?; - let result = env.call_method( + env.call_method( &activity, jni::jni_str!("gpuiHideKeyboard"), jni::jni_sig!("(J)V"), &[JValue::Long(session as i64)], - ); - env.exception_clear(); - result.map_err(|e| e.to_string())?; + ) + .or_clear(env)?; Ok(()) - }); + }) { + log::debug!("gpuiHideKeyboard: {err}"); + } if let Some(app) = android_app() { app.hide_soft_input(false); } @@ -1578,18 +1654,19 @@ pub fn hide_keyboard_android() { pub(super) fn reset_keyboard_composition() { let session = super::text_input::new_session(); - let _ = with_env(|env| { + if let Err(err) = with_env(|env| { let activity = activity(env)?; - let result = env.call_method( + env.call_method( &activity, jni::jni_str!("gpuiResetComposition"), jni::jni_sig!("(J)V"), &[JValue::Long(session as i64)], - ); - env.exception_clear(); - result.map_err(|e| e.to_string())?; + ) + .or_clear(env)?; Ok(()) - }); + }) { + log::debug!("gpuiResetComposition: {err}"); + } } /// Receive Java InputConnection updates without touching GPUI on the UI thread. diff --git a/src/android/platform.rs b/src/android/platform.rs index 93b1c95b..d4885329 100644 --- a/src/android/platform.rs +++ b/src/android/platform.rs @@ -849,14 +849,14 @@ impl AndroidPlatform { /// /// Returns `None` if the JNI environment is unavailable or the call fails. fn query_keyboard_layout_id_via_jni(&self) -> Option { - use crate::android::jni::{self as jni_helpers, get_string}; + use crate::android::jni::{self as jni_helpers, get_string, JniResultExt as _}; use jni::objects::JValue; jni_helpers::with_env(|env| { let activity = jni_helpers::activity(env)?; // activity.getSystemService("input_method") - let service_name = env.new_string("input_method").map_err(|e| e.to_string())?; + let service_name = env.new_string("input_method").or_clear(env)?; let imm = env .call_method( &activity, @@ -865,7 +865,7 @@ impl AndroidPlatform { &[JValue::Object(&service_name)], ) .and_then(|v| v.l()) - .map_err(|e| e.to_string())?; + .or_clear(env)?; if imm.is_null() { return Ok(None); } @@ -879,7 +879,7 @@ impl AndroidPlatform { &[], ) .and_then(|v| v.l()) - .map_err(|e| e.to_string())?; + .or_clear(env)?; if subtype.is_null() { return Ok(None); } @@ -893,7 +893,7 @@ impl AndroidPlatform { &[], ) .and_then(|v| v.l()) - .map_err(|e| e.to_string())?; + .or_clear(env)?; let result = get_string(env, &locale_obj).replace('_', "-"); if result.is_empty() { @@ -952,15 +952,15 @@ impl AndroidPlatform { /// Returns -1 on failure (JNI unavailable, API < 29, etc.). #[allow(dead_code)] fn query_thermal_status_via_jni(&self) -> i32 { - use crate::android::jni as jni_helpers; + use crate::android::jni::{self as jni_helpers, JniResultExt as _}; use jni::objects::JValue; jni_helpers::with_env(|env| { let activity = jni_helpers::activity(env)?; // activity.getSystemService("power") - let service_name = env.new_string("power").map_err(|e| e.to_string())?; - let pm = match env + let service_name = env.new_string("power").or_clear(env)?; + let pm = env .call_method( &activity, jni::jni_str!("getSystemService"), @@ -968,16 +968,13 @@ impl AndroidPlatform { &[JValue::Object(&service_name)], ) .and_then(|v| v.l()) - { - Ok(o) if !o.is_null() => o, - _ => { - env.exception_clear(); - return Err("getSystemService(power) failed or returned null".to_string()); - } - }; + .or_clear(env)?; + if pm.is_null() { + return Err("getSystemService(power) returned null".to_string()); + } // pm.getCurrentThermalStatus() — API 29+ - let status = match env + let status = env .call_method( &pm, jni::jni_str!("getCurrentThermalStatus"), @@ -985,13 +982,7 @@ impl AndroidPlatform { &[], ) .and_then(|v| v.i()) - { - Ok(s) => s, - Err(_) => { - env.exception_clear(); - return Err("getCurrentThermalStatus() failed".to_string()); - } - }; + .or_clear(env)?; log::trace!("query_thermal_status_via_jni: status={}", status); Ok(status) diff --git a/src/android/platform_view.rs b/src/android/platform_view.rs index 646cde32..34bb1286 100644 --- a/src/android/platform_view.rs +++ b/src/android/platform_view.rs @@ -49,7 +49,7 @@ impl AndroidPlatformView { /// Create the native Android View via JNI. fn create_native_view(&self, params: &PlatformViewParams) -> Result<(), String> { - use super::jni::{activity, find_app_class, with_env, JniExt}; + use super::jni::{activity, find_app_class, with_env, JniExt, JniResultExt as _}; use jni::objects::JValue; with_env(|env| { @@ -93,10 +93,8 @@ impl AndroidPlatformView { JValue::Object(&creation_params_jstr), ], ) - .map_err(|e| { - env.exception_clear(); - format!("createView JNI call failed: {}", e) - })?; + .or_clear(env) + .map_err(|e| format!("createView JNI call failed: {}", e))?; let success = result.z().unwrap_or(false); if !success { @@ -114,77 +112,71 @@ impl AndroidPlatformView { /// Update the native view's bounds via JNI. fn update_native_bounds(&self, bounds: &PlatformViewBounds) { - use super::jni::{find_app_class, with_env}; + use super::jni::{find_app_class, with_env, JniResultExt as _}; use jni::objects::JValue; let view_id = self.id.0 as i64; - let _ = with_env(|env| { + if let Err(err) = with_env(|env| { let helper_class = find_app_class(env, "dev.gpui.mobile.GpuiPlatformView")?; - let _ = env - .call_static_method( - &helper_class, - jni::jni_str!("setBounds"), - jni::jni_sig!("(JFFFF)V"), - &[ - JValue::Long(view_id), - JValue::Float(bounds.x), - JValue::Float(bounds.y), - JValue::Float(bounds.width), - JValue::Float(bounds.height), - ], - ) - .map_err(|e| { - env.exception_clear(); - format!("setBounds failed: {}", e) - })?; + env.call_static_method( + &helper_class, + jni::jni_str!("setBounds"), + jni::jni_sig!("(JFFFF)V"), + &[ + JValue::Long(view_id), + JValue::Float(bounds.x), + JValue::Float(bounds.y), + JValue::Float(bounds.width), + JValue::Float(bounds.height), + ], + ) + .or_clear(env)?; Ok(()) - }); + }) { + log::warn!("AndroidPlatformView: setBounds: {err}"); + } } /// Update the native view's visibility via JNI. fn update_native_visibility(&self, visible: bool) { - use super::jni::{find_app_class, with_env}; + use super::jni::{find_app_class, with_env, JniResultExt as _}; use jni::objects::JValue; let view_id = self.id.0 as i64; - let _ = with_env(|env| { + if let Err(err) = with_env(|env| { let helper_class = find_app_class(env, "dev.gpui.mobile.GpuiPlatformView")?; - let _ = env - .call_static_method( - &helper_class, - jni::jni_str!("setVisible"), - jni::jni_sig!("(JZ)V"), - &[JValue::Long(view_id), JValue::Bool(visible)], - ) - .map_err(|e| { - env.exception_clear(); - format!("setVisible failed: {}", e) - })?; + env.call_static_method( + &helper_class, + jni::jni_str!("setVisible"), + jni::jni_sig!("(JZ)V"), + &[JValue::Long(view_id), JValue::Bool(visible)], + ) + .or_clear(env)?; Ok(()) - }); + }) { + log::warn!("AndroidPlatformView: setVisible: {err}"); + } } /// Remove and dispose the native view via JNI. fn dispose_native_view(&self) { - use super::jni::{find_app_class, with_env}; + use super::jni::{find_app_class, with_env, JniResultExt as _}; use jni::objects::JValue; let view_id = self.id.0 as i64; - let _ = with_env(|env| { + if let Err(err) = with_env(|env| { let helper_class = find_app_class(env, "dev.gpui.mobile.GpuiPlatformView")?; - let _ = env - .call_static_method( - &helper_class, - jni::jni_str!("disposeView"), - jni::jni_sig!("(J)V"), - &[JValue::Long(view_id)], - ) - .map_err(|e| { - env.exception_clear(); - format!("disposeView failed: {}", e) - })?; + env.call_static_method( + &helper_class, + jni::jni_str!("disposeView"), + jni::jni_sig!("(J)V"), + &[JValue::Long(view_id)], + ) + .or_clear(env)?; Ok(()) - }); + }) { + log::warn!("AndroidPlatformView: disposeView: {err}"); + } } } @@ -216,25 +208,23 @@ impl PlatformView for AndroidPlatformView { if self.disposed.load(Ordering::Relaxed) { return; } - use super::jni::{find_app_class, with_env}; + use super::jni::{find_app_class, with_env, JniResultExt as _}; use jni::objects::JValue; let view_id = self.id.0 as i64; - let _ = with_env(|env| { + if let Err(err) = with_env(|env| { let helper_class = find_app_class(env, "dev.gpui.mobile.GpuiPlatformView")?; - let _ = env - .call_static_method( - &helper_class, - jni::jni_str!("setZIndex"), - jni::jni_sig!("(JI)V"), - &[JValue::Long(view_id), JValue::Int(z_index)], - ) - .map_err(|e| { - env.exception_clear(); - format!("setZIndex failed: {}", e) - })?; + env.call_static_method( + &helper_class, + jni::jni_str!("setZIndex"), + jni::jni_sig!("(JI)V"), + &[JValue::Long(view_id), JValue::Int(z_index)], + ) + .or_clear(env)?; Ok(()) - }); + }) { + log::warn!("AndroidPlatformView: setZIndex: {err}"); + } } fn dispose(&self) { diff --git a/src/android/window.rs b/src/android/window.rs index b602f37c..0ded0ae3 100644 --- a/src/android/window.rs +++ b/src/android/window.rs @@ -1976,18 +1976,18 @@ impl PlatformWindow for AndroidPlatformWindow { // CursorAnchorInfo built from the given bounds. // Requires API level 21+ (Lollipop). - use crate::android::jni as jni_helpers; + use crate::android::jni::{self as jni_helpers, JniResultExt as _}; use jni::objects::JValue; let x: f32 = bounds.origin.x.into(); let y: f32 = bounds.origin.y.into(); let h: f32 = bounds.size.height.into(); - let _ = jni_helpers::with_env(|env| { + if let Err(err) = jni_helpers::with_env(|env| { let activity = jni_helpers::activity(env)?; // 1. Get InputMethodManager - let service_name = env.new_string("input_method").map_err(|e| e.to_string())?; + let service_name = env.new_string("input_method").or_clear(env)?; let imm = env .call_method( &activity, @@ -1996,10 +1996,7 @@ impl PlatformWindow for AndroidPlatformWindow { &[JValue::Object(&service_name)], ) .and_then(|v| v.l()) - .map_err(|e| { - env.exception_clear(); - e.to_string() - })?; + .or_clear(env)?; if imm.is_null() { return Err("getSystemService returned null".to_string()); } @@ -2011,12 +2008,9 @@ impl PlatformWindow for AndroidPlatformWindow { jni::jni_sig!("()V"), &[], ) - .map_err(|e| { - env.exception_clear(); - e.to_string() - })?; + .or_clear(env)?; - let _ = env.call_method( + env.call_method( &builder, jni::jni_str!("setInsertionMarkerLocation"), jni::jni_sig!("(FFFFI)Landroid/view/inputmethod/CursorAnchorInfo$Builder;"), @@ -2027,8 +2021,8 @@ impl PlatformWindow for AndroidPlatformWindow { JValue::Float(y + h), JValue::Int(0), ], - ); - env.exception_clear(); + ) + .or_clear(env)?; let anchor_info = env .call_method( @@ -2038,10 +2032,7 @@ impl PlatformWindow for AndroidPlatformWindow { &[], ) .and_then(|v| v.l()) - .map_err(|e| { - env.exception_clear(); - e.to_string() - })?; + .or_clear(env)?; if anchor_info.is_null() { return Err("CursorAnchorInfo.build() returned null".to_string()); } @@ -2055,10 +2046,7 @@ impl PlatformWindow for AndroidPlatformWindow { &[], ) .and_then(|v| v.l()) - .map_err(|e| { - env.exception_clear(); - e.to_string() - })?; + .or_clear(env)?; if window.is_null() { return Err("getWindow() returned null".to_string()); } @@ -2071,27 +2059,26 @@ impl PlatformWindow for AndroidPlatformWindow { &[], ) .and_then(|v| v.l()) - .map_err(|e| { - env.exception_clear(); - e.to_string() - })?; + .or_clear(env)?; if decor_view.is_null() { return Err("getDecorView() returned null".to_string()); } // 4. imm.updateCursorAnchorInfo(view, info) - let _ = env.call_method( + env.call_method( &imm, jni::jni_str!("updateCursorAnchorInfo"), jni::jni_sig!("(Landroid/view/View;Landroid/view/inputmethod/CursorAnchorInfo;)V"), &[JValue::Object(&decor_view), JValue::Object(&anchor_info)], - ); - env.exception_clear(); + ) + .or_clear(env)?; log::trace!("update_ime_position: x={:.0} y={:.0} h={:.0}", x, y, h); Ok(()) - }); + }) { + log::debug!("update_ime_position: {err}"); + } } }