//! The JNI bridge behind E3's two Java stub classes. See `Cargo.toml`'s //! package comment for what this crate is and RUST.md's E3 entry for the //! design decisions. //! //! Each native method is declared with `jni`'s [`native_method!`] macro //! rather than a hand-written `#[no_mangle] extern "system" fn Java_...`: //! the macro derives the mangled export name and the JNI signature from the //! Rust function itself, so the two cannot drift apart the way a //! hand-typed name string and a hand-typed `"(Landroid/...;)V"` signature //! routinely do. `error_policy = LogErrorAndDefault` matches //! `Notifications.kt`'s own posture: a failure here (a lost connection, a //! JNI call that threw) is reported to logcat, not thrown back into Java //! as an exception that would crash the app over something recoverable. //! //! Each `const _: NativeMethod = native_method! { ... };` binding is //! otherwise unused by name -- `_` is the idiomatic way to keep a //! side-effecting const (here, generating the `#[export_name]`d function //! the JVM resolves by the JNI naming convention) without a `dead_code` //! warning for a binding nothing reads. mod jcall; mod notify; mod settings; mod share; use jni::errors::LogErrorAndDefault; use jni::objects::{JClass, JObject}; use jni::sys::jint; use jni::{Env, NativeMethod, native_method}; /// Installs the `log` backend that routes to logcat, once per process. /// Without it, `LogErrorAndDefault` (every native method below) and any /// `log::error!` inside `jni` itself (e.g. `JString`'s `Display` fallback) /// call into the `log` facade's default no-op logger, and a real failure /// vanishes with nothing on logcat to say so -- silently *more* wrong than /// crashing, since nothing on screen or in the log says a notification was /// dropped. Called from every entry point below rather than a Java-side /// `Application.onCreate`, since this crate deliberately has no such class /// to hook (see RUST.md's E3 entry on the two-Java-classes floor). fn ensure_logger() { static ONCE: std::sync::Once = std::sync::Once::new(); ONCE.call_once(|| { #[cfg(target_os = "android")] android_logger::init_once( android_logger::Config::default() .with_max_level(log::LevelFilter::Debug) .with_tag("android-shell"), ); }); } // The parameters are spelled as their Java types, not as `JObject`: the // macro encodes each argument into the exported symbol's JNI signature // (and JNI resolves `Java_...` names *by* that signature), so a generic // `JObject` here would export `(Ljava/lang/Object;...)` against a Java // method actually declared `(Landroid/app/Activity;...)` -- two different // symbols that never resolve to each other, silently, with no compiler // error on either side. `android.app.Activity` etc. have no dedicated // Rust wrapper in this crate, so they fall back to plain `JObject` in the // implementation functions below (the "Built-in Types" note in // `native_method!`'s docs). const _: NativeMethod = native_method! { java_type = "com.example.aiapp.shell.MainActivity", static extern fn native_handle_intent(activity: android.app.Activity, intent: android.content.Intent) -> (), error_policy = LogErrorAndDefault, }; /// `MainActivity.nativeHandleIntent` -- called from `onCreate` and /// `onNewIntent`. See `share::handle_intent` for what an intent can mean. fn native_handle_intent<'local>( env: &mut Env<'local>, _class: JClass<'local>, activity: JObject<'local>, intent: JObject<'local>, ) -> Result<(), jni::errors::Error> { ensure_logger(); jcall::remember_class_loader(env, &activity)?; share::handle_intent(env, &activity, &intent) } const _: NativeMethod = native_method! { java_type = "com.example.aiapp.shell.NotificationService", static extern fn native_sync(context: android.content.Context) -> (), error_policy = LogErrorAndDefault, }; /// `NotificationService.nativeSync` -- called both from `MainActivity` (an /// enrollment may have just landed) and from `NotificationService.sync` /// itself. See `notify::sync`. fn native_sync<'local>( env: &mut Env<'local>, _class: JClass<'local>, context: JObject<'local>, ) -> Result<(), jni::errors::Error> { ensure_logger(); jcall::remember_class_loader(env, &context)?; notify::sync(env, &context) } const _: NativeMethod = native_method! { java_type = "com.example.aiapp.shell.NotificationService", static extern fn native_on_start_command(service: android.app.Service) -> jint, error_policy = LogErrorAndDefault, }; /// `NotificationService.nativeOnStartCommand`. See `notify::on_start_command`. fn native_on_start_command<'local>( env: &mut Env<'local>, _class: JClass<'local>, service: JObject<'local>, ) -> Result { ensure_logger(); jcall::remember_class_loader(env, &service)?; Ok(notify::on_start_command(env, service)) } const _: NativeMethod = native_method! { java_type = "com.example.aiapp.shell.NotificationService", static extern fn native_on_destroy() -> (), error_policy = LogErrorAndDefault, }; /// `NotificationService.nativeOnDestroy`. See `notify::on_destroy`. fn native_on_destroy<'local>( _env: &mut Env<'local>, _class: JClass<'local>, ) -> Result<(), jni::errors::Error> { ensure_logger(); notify::on_destroy(); Ok(()) }