Open the sign-in URL through an ACTION_VIEW Intent on Android
The previous commit made the missing launcher honest; this gives Android a real one, so Login Flow v2 can complete on device. Builds `new Intent(ACTION_VIEW, Uri.parse(url))` and hands it to `startActivity` over JNI. The JavaVM and Activity come from ndk_context, which android-activity's glue populates at startup — the same handle Slint's backend uses, so there is no second VM to reconcile. The login worker is a plain std::thread and therefore unknown to the JVM, where any JNI call would abort the process. jni 0.22 scopes attachment to a closure rather than returning a guard, so the whole Intent is built and dispatched inside `attach_current_thread` and the thread detaches on the way out. Names use `jni_str!` and signatures `jni_sig!`, both compile-time: a typo is a build error rather than a NoSuchMethodError on the device. A pending Java exception is checked and cleared before returning, since leaving one pending makes the next JNI call fail somewhere unrelated; in practice it means ActivityNotFoundException, i.e. no browser installed. jni is pinned to 0.22 to match Slint's Android backend. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Generated
+2
@@ -1469,7 +1469,9 @@ dependencies = [
|
||||
"dr-sync-nextcloud",
|
||||
"dr-thumbs",
|
||||
"dr-types",
|
||||
"jni 0.22.4",
|
||||
"log",
|
||||
"ndk-context",
|
||||
"pollster",
|
||||
"reqwest",
|
||||
"rusqlite",
|
||||
|
||||
@@ -45,6 +45,12 @@ slint = { workspace = true, features = ["backend-winit"] }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
slint = { workspace = true, features = ["backend-android-activity-06"] }
|
||||
# Opening the sign-in URL needs an ACTION_VIEW Intent — Android has no
|
||||
# xdg-open. Version-matched to Slint's Android backend so both halves of the
|
||||
# process agree on the JavaVM types; ndk-context supplies the VM and activity
|
||||
# that android-activity's glue already stashed.
|
||||
jni = "0.22"
|
||||
ndk-context = "0.1"
|
||||
|
||||
[build-dependencies]
|
||||
slint-build.workspace = true
|
||||
|
||||
@@ -553,7 +553,14 @@ fn open_in_browser(url: &str) -> std::io::Result<()> {
|
||||
.spawn()
|
||||
.map(|_| ())
|
||||
}
|
||||
#[cfg(not(all(unix, not(target_os = "android"), not(target_os = "macos"))))]
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
android_open_url(url).map_err(|e| std::io::Error::other(e))
|
||||
}
|
||||
#[cfg(not(any(
|
||||
all(unix, not(target_os = "android"), not(target_os = "macos")),
|
||||
target_os = "android"
|
||||
)))]
|
||||
{
|
||||
let _ = url;
|
||||
Err(std::io::Error::new(
|
||||
@@ -562,3 +569,76 @@ fn open_in_browser(url: &str) -> std::io::Result<()> {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Hand a URL to whatever the user has set as their browser, via
|
||||
/// `startActivity(new Intent(ACTION_VIEW, Uri.parse(url)))`.
|
||||
///
|
||||
/// Called from the login worker, which is a plain `std::thread` and therefore
|
||||
/// not known to the JVM — every JNI call from it would abort the process
|
||||
/// without `attach_current_thread` first. The thread detaches when the returned
|
||||
/// guard drops.
|
||||
///
|
||||
/// The VM and activity come from `ndk_context`, which android-activity's glue
|
||||
/// populates at startup; that is the same handle Slint's backend uses, so there
|
||||
/// is no second JavaVM to reconcile.
|
||||
#[cfg(target_os = "android")]
|
||||
fn android_open_url(url: &str) -> Result<(), String> {
|
||||
let ctx = ndk_context::android_context();
|
||||
if ctx.vm().is_null() || ctx.context().is_null() {
|
||||
return Err("no Android context available".into());
|
||||
}
|
||||
|
||||
// SAFETY: the pointer comes from ndk_context, which android-activity fills
|
||||
// in with the process's real JavaVM before any Rust runs.
|
||||
let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) };
|
||||
let raw_activity: jni::sys::jobject = ctx.context().cast();
|
||||
|
||||
// jni 0.22 scopes the attachment to a closure rather than handing back a
|
||||
// guard, so all the JNI work happens in here and the thread is detached on
|
||||
// the way out.
|
||||
vm.attach_current_thread(|env| {
|
||||
// SAFETY: valid for as long as this frame, which is all we need — the
|
||||
// Intent is dispatched before the closure returns.
|
||||
let activity = unsafe { jni::objects::JObject::from_raw(env, raw_activity) };
|
||||
|
||||
// Names go through `jni_str!` (UTF-8 literal to MUTF-8 `&'static
|
||||
// JNIStr`) and signatures through `jni_sig!`, which parses and
|
||||
// type-checks them at compile time — a typo in either is a build error
|
||||
// rather than a NoSuchMethodError on the device.
|
||||
let jurl = env.new_string(url)?;
|
||||
let uri = env
|
||||
.call_static_method(
|
||||
jni::jni_str!("android/net/Uri"),
|
||||
jni::jni_str!("parse"),
|
||||
jni::jni_sig!("(Ljava/lang/String;)Landroid/net/Uri;"),
|
||||
&[(&jurl).into()],
|
||||
)?
|
||||
.l()?;
|
||||
|
||||
let action = env.new_string("android.intent.action.VIEW")?;
|
||||
let intent = env.new_object(
|
||||
jni::jni_str!("android/content/Intent"),
|
||||
jni::jni_sig!("(Ljava/lang/String;Landroid/net/Uri;)V"),
|
||||
&[(&action).into(), (&uri).into()],
|
||||
)?;
|
||||
|
||||
env.call_method(
|
||||
&activity,
|
||||
jni::jni_str!("startActivity"),
|
||||
jni::jni_sig!("(Landroid/content/Intent;)V"),
|
||||
&[(&intent).into()],
|
||||
)?;
|
||||
|
||||
// A pending Java exception leaves the JVM unusable for the next call,
|
||||
// and ActivityNotFoundException here means the device has no browser at
|
||||
// all — worth reporting rather than leaving for something unrelated to
|
||||
// trip over.
|
||||
if env.exception_check() {
|
||||
env.exception_clear();
|
||||
return Err(jni::errors::Error::JavaException);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.map_err(|e: jni::errors::Error| e.to_string())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user