//! What the app was launched with, and handing a finished export back out. //! //! FR-PLAT-AND-6's Rust side, which is deliberately the thin side. Both //! directions are implemented in `android/java/paris/tourolle/darkroom/` and //! everything here is the two calls that reach them; `Intents.java` carries the //! reasoning for the split. The short version is that a JNI method signature is //! a string Java resolves at run time and nothing checks at build time, so //! forty of them is forty ways for a rename to become a `NoSuchMethodError` on //! somebody's tablet. Two is two. //! //! # Nothing here fails loudly //! //! A class the loader cannot see, a pending Java exception, a shared URI whose //! grant died with the task that received it: each ends as a log line and an //! empty result. This runs on the way to [`dr_ui::run`], before a window //! exists, and the alternative to opening with an empty browsing list is not //! opening at all. use std::path::{Path, PathBuf}; use jni::errors::Result as JniResult; use jni::objects::{JClass, JObject, JObjectArray, JString, JValue}; use jni::{JNIEnv, JavaVM}; /// The class both directions live in, named the way `loadClass` wants it — /// dots, not slashes. `find_class` takes the other form, and this code calls /// neither by accident; see [`load_class`]. const INTENTS: &str = "paris.tourolle.darkroom.Intents"; /// The images this launch was asked to open, already local and readable. /// /// Empty for an ordinary launch from the launcher, which is the common case /// and not a failure. What comes back is passed to `dr_ui::run` exactly as /// command-line paths are on the desktop, so a shared photograph becomes the /// browsing list and `startup_action` shows it rather than the launch screen. pub fn launch_images(app: &slint::android::AndroidApp) -> Vec { with_activity(app, "reading the launch intent", |env, activity| { let class = load_class(env, activity, INTENTS)?; let returned = env .call_static_method( &class, "receive", "(Landroid/app/Activity;)[Ljava/lang/String;", &[JValue::Object(activity)], )? .l()?; let array = JObjectArray::from(returned); let count = env.get_array_length(&array)?; let mut paths = Vec::with_capacity(count as usize); for i in 0..count { let element = env.get_object_array_element(&array, i)?; let text: String = env.get_string(&JString::from(element))?.into(); paths.push(PathBuf::from(text)); } Ok(paths) }) .unwrap_or_default() } /// Offer a file this app produced to whatever else is installed. /// /// `false` means the sheet did not open — the file is not under the directory /// [`ExportProvider`] serves, or nothing installed accepts the type. Both are /// answers a caller has to be able to give the user, because a share control /// that silently does nothing is indistinguishable from one that failed. /// /// **This half has no caller yet, and that is the honest state of it.** The /// provider, the URI grant and the chooser are all here and are what /// FR-PLAT-AND-6 asks for; what is missing is a share control in the interface, /// which lives in `ui/dr-ui` and needs one thing this signature shows: an /// `AndroidApp` to call through. Wiring it means keeping a clone of the app — /// it is `Clone` and cheap — somewhere `ui/` can reach, which is a change to /// how the platform entry point talks to the interface rather than a change /// here. Until that exists this function is reachable and untested, and it is /// deliberately not tagged as covering the requirement. /// /// `mime` decides which applications the chooser offers; the empty string /// falls back to `image/*` on the Java side. pub fn share(app: &slint::android::AndroidApp, file: &Path, mime: &str) -> bool { with_activity(app, "opening the share sheet", |env, activity| { let class = load_class(env, activity, INTENTS)?; let path = env.new_string(file.to_string_lossy().as_ref())?; let mime = env.new_string(mime)?; env.call_static_method( &class, "share", "(Landroid/app/Activity;Ljava/lang/String;Ljava/lang/String;)Z", &[ JValue::Object(activity), JValue::Object(&path), JValue::Object(&mime), ], )? .z() }) .unwrap_or(false) } /// Attach to the JVM, borrow the activity, and run `body` against both. /// /// Shared by the two entry points because the three steps before the /// interesting one are identical and each has its own way of failing. `body` /// returning `Err` is reported here, once, in the one place that can also clear /// a pending Java exception — see [`report`]. fn with_activity( app: &slint::android::AndroidApp, doing: &str, body: impl FnOnce(&mut JNIEnv, &JObject) -> JniResult, ) -> Option { let vm = match unsafe { JavaVM::from_raw(app.vm_as_ptr().cast()) } { Ok(vm) => vm, Err(e) => { log::error!("no JVM handle, so {doing} is skipped: {e}"); return None; } }; // Cheap when the thread is already attached, which it is: the glue // attached it before it called `android_main`. The guard exists for the // case where it is not, and costs a lookup where it is. let mut env = match vm.attach_current_thread() { Ok(env) => env, Err(e) => { log::error!("cannot attach to the JVM, so {doing} is skipped: {e}"); return None; } }; // SAFETY: `activity_as_ptr` documents this as an unowned JNI *global* // reference to the Activity, valid for as long as the `AndroidApp` it came // from. `JObject` in jni 0.21 is a plain wrapper with no `Drop`, so // borrowing it here cannot delete a reference this code does not own — the // one way to get this wrong is `AutoLocal` or a `GlobalRef`, both of which // would free it out from under android-activity. let activity = unsafe { JObject::from_raw(app.activity_as_ptr().cast()) }; match body(&mut env, &activity) { Ok(value) => Some(value), Err(e) => { report(&mut env, doing, &e); None } } } /// Look an app class up through the *activity's* class loader. /// /// `find_class` is the obvious call and the wrong one. JNI resolves a class /// against the loader belonging to the Java frame beneath the call, and on this /// thread there is no such frame: `android_main` runs on a thread the native /// glue created and attached itself, so the loader in scope is the system one. /// It knows every class in the platform and nothing at all from this APK, and /// says so as a `ClassNotFoundException` naming a class that is plainly in the /// dex — which reads as a broken build rather than as the wrong loader. /// /// The activity is a Java object, so its loader is the app's. fn load_class<'local>( env: &mut JNIEnv<'local>, activity: &JObject, name: &str, ) -> JniResult> { let loader = env .call_method(activity, "getClassLoader", "()Ljava/lang/ClassLoader;", &[])? .l()?; let name = env.new_string(name)?; let class = env .call_method( &loader, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;", &[JValue::Object(&name)], )? .l()?; Ok(JClass::from(class)) } /// Log a JNI failure, and clear the exception behind it if there is one. /// /// The clearing is not tidiness. A Java exception raised through JNI stays /// *pending* on the thread, and the next JNI call made while one is pending /// aborts the process — so a swallowed exception here would come back as a /// crash somewhere unrelated, most likely inside Slint. `exception_describe` /// first, because the trace it prints to logcat is the only place the Java /// class and line survive; `jni::errors::Error::JavaException` on its own says /// neither. fn report(env: &mut JNIEnv, doing: &str, e: &jni::errors::Error) { log::error!("{doing} failed: {e}"); if let Ok(true) = env.exception_check() { let _ = env.exception_describe(); let _ = env.exception_clear(); } }