//! The JNI half of `DevLogProvider`: reading this process's own log ring //! for a `ContentProvider` that Dev Updater queries. use android_view::jni::JNIEnv; use android_view::jni::objects::{JClass, JObject, JString}; use android_view::jni::sys::{jlong, jobjectArray}; use std::{path::Path, ptr, sync::OnceLock}; /// Gated with its one reader: the tabs demo links no `client-core` and so /// has no ring to lay out, and an ungated constant is a warning in that /// build (`iris-android-app` without `transcript-screen`). #[cfg(feature = "transcript-screen")] const FIELDS_PER_LINE: usize = 5; /// The authority the provider registered itself under, once it has been /// created. `None` until then, which is a state worth being able to say: /// a provider Android never instantiated and one that is answering look /// the same from inside this process otherwise. static AUTHORITY: OnceLock = OnceLock::new(); #[cfg(feature = "bench")] pub fn authority() -> Option<&'static str> { AUTHORITY.get().map(String::as_str) } /// The directory is taken here as well as in /// `MainActivity.nativeSetFilesDir` because **the provider is often the /// only thing running**: once the app has died, Dev Updater's query /// starts the process for the provider alone, so no activity ever runs /// and the panic hook's file would never be replayed into the ring. That /// is precisely the run whose log is being asked for. Whichever of the /// two arrives first does the replay; `set_crash_dir` deletes the file, /// so the second finds nothing and says nothing. /// /// # Safety /// Called by the JVM with the arguments its `native` declaration names. #[unsafe(no_mangle)] pub extern "system" fn Java_dev_iris_android_demo_DevLogProvider_nativeReady( mut env: JNIEnv, _class: JClass, authority: JString, files_dir: JString, ) { #[cfg(feature = "transcript-screen")] if let Some(dir) = string_arg(&mut env, &files_dir) { crate::android::app_log::set_crash_dir(Path::new(&dir)); } #[cfg(not(feature = "transcript-screen"))] let _ = &files_dir; let Some(authority) = string_arg(&mut env, &authority) else { return; }; log::info!("iris devlog: serving this app's log at content://{authority}"); let _ = AUTHORITY.set(authority); } fn string_arg(env: &mut JNIEnv, value: &JString) -> Option { if value.is_null() { return None; } env.get_string(value).ok().map(Into::into) } /// `newest_seq` is `-1` for a ring nothing has been written to, which is /// what tells a reader holding a cursor that this process **restarted**: /// the ring is in memory, so a new process starts again at zero and a /// stale cursor would otherwise skip everything silently. /// /// # Safety /// Called by the JVM with the arguments its `native` declaration names. #[unsafe(no_mangle)] pub extern "system" fn Java_dev_iris_android_demo_DevLogProvider_nativeStatus( mut env: JNIEnv, _class: JClass, ) -> jobjectArray { string_array(&mut env, &status_fields()) } /// Inclusive of `since` because [`crate::client::log_ring::LogRing::since`] /// is, and one definition of the cursor is what keeps the app's own /// uploaded report and this provider describing the same lines. /// /// # Safety /// Called by the JVM with the arguments its `native` declaration names. #[unsafe(no_mangle)] pub extern "system" fn Java_dev_iris_android_demo_DevLogProvider_nativeLinesSince( mut env: JNIEnv, _class: JClass, since: jlong, ) -> jobjectArray { // A negative cursor is a caller asking for everything, not an error to // take the app down over: the provider is a diagnostic. string_array(&mut env, &line_fields(since.max(0) as u64)) } #[cfg(feature = "transcript-screen")] fn status_fields() -> Vec { let ring = crate::client::log_ring::process_ring(); vec![ ring.len().to_string(), ring.dropped().to_string(), ring.newest_seq().map_or(-1, |seq| seq as i64).to_string(), ] } /// The tabs demo links no `client-core` and keeps no ring, so it holds /// nothing and has never dropped anything -- which is the truth, not a /// stand-in. The natives are still exported there, because a `native` /// method Java declares and the library does not is an /// `UnsatisfiedLinkError` the moment the class loads. #[cfg(not(feature = "transcript-screen"))] fn status_fields() -> Vec { vec!["0".to_string(), "0".to_string(), "-1".to_string()] } #[cfg(feature = "transcript-screen")] fn line_fields(since: u64) -> Vec { let (lines, _next) = crate::client::log_ring::process_ring().since(since); let mut fields = Vec::with_capacity(lines.len() * FIELDS_PER_LINE); for line in lines { fields.push(line.seq.to_string()); fields.push(line.at_ms.to_string()); fields.push(line.level.to_string()); fields.push(line.target); fields.push(line.message); } fields } #[cfg(not(feature = "transcript-screen"))] fn line_fields(_since: u64) -> Vec { Vec::new() } /// Null rather than a panic across the JNI boundary: `DevLogProvider` /// reads it as "the provider could not answer" and returns no cursor, /// which Dev Updater already draws as a distinct state. Taking the app /// down to report that its diagnostic is unavailable would be worse than /// the diagnostic being unavailable. fn string_array(env: &mut JNIEnv, fields: &[String]) -> jobjectArray { let null = ptr::null_mut(); let Ok(class) = env.find_class("java/lang/String") else { return null; }; let Ok(array) = env.new_object_array(fields.len() as i32, class, JObject::null()) else { return null; }; for (index, field) in fields.iter().enumerate() { let Ok(value) = env.new_string(field) else { return null; }; if env .set_object_array_element(&array, index as i32, value) .is_err() { return null; } } array.into_raw() }